├── util ├── __init__.py ├── styler.py ├── helpers.py ├── log.py └── validation.py ├── .gitignore ├── docs ├── assets │ ├── favicon.ico │ ├── images │ │ ├── attacks1.png │ │ ├── attacks2.png │ │ ├── attacks3.png │ │ ├── browser.png │ │ ├── manual.png │ │ └── dashboard.png │ ├── flag-line.svg │ └── demos │ │ ├── server-no-clients.cast │ │ ├── client-no-exploits.cast │ │ └── client.cast ├── user-manual │ ├── dashboard │ │ ├── dashboard.md │ │ ├── overview.md │ │ ├── manual.md │ │ └── browser.md │ ├── client │ │ ├── running.md │ │ ├── overview.md │ │ ├── configuration.md │ │ ├── exploit-guideline.md │ │ └── exploit-management.md │ └── server │ │ ├── running.md │ │ ├── overview.md │ │ ├── submitter-guideline.md │ │ └── configuration.md ├── troubleshooting.md ├── index.md ├── install.md └── quickstart.md ├── web ├── public │ └── favicon.ico ├── src │ ├── main.js │ ├── game.js │ ├── socket.js │ ├── api.js │ ├── timers.js │ ├── counters.js │ ├── App.vue │ └── components │ │ ├── Navigation.vue │ │ ├── ManualSubmit.vue │ │ ├── Dashboard.vue │ │ ├── Analytics.vue │ │ └── FlagBrowser.vue ├── vite.config.js ├── index.html ├── .gitignore └── package.json ├── database.py ├── setup.py ├── models.py ├── mkdocs.yml ├── cli.py ├── README.md ├── handler.py ├── dsl.py ├── runner.py ├── client.py └── server.py /util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | *.egg-info 4 | dist 5 | site -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/favicon.ico -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /docs/assets/images/attacks1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/images/attacks1.png -------------------------------------------------------------------------------- /docs/assets/images/attacks2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/images/attacks2.png -------------------------------------------------------------------------------- /docs/assets/images/attacks3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/images/attacks3.png -------------------------------------------------------------------------------- /docs/assets/images/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/images/browser.png -------------------------------------------------------------------------------- /docs/assets/images/manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/images/manual.png -------------------------------------------------------------------------------- /docs/assets/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusanlazic/fast/HEAD/docs/assets/images/dashboard.png -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /web/src/game.js: -------------------------------------------------------------------------------- 1 | import api from '@/api.js' 2 | 3 | export const game = { 4 | async initialize() { 5 | this.flagFormat = (await api.getFlagFormat()).format 6 | } 7 | } -------------------------------------------------------------------------------- /docs/assets/flag-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | from peewee import Model, Proxy, SqliteDatabase 2 | 3 | db = Proxy() 4 | 5 | 6 | class BaseModel(Model): 7 | class Meta: 8 | database = db 9 | 10 | 11 | fallbackdb = SqliteDatabase('fallback.db') 12 | 13 | 14 | class FallbackBaseModel(Model): 15 | class Meta: 16 | database = fallbackdb 17 | -------------------------------------------------------------------------------- /web/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | ], 11 | resolve: { 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)) 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fast Server Dashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/src/socket.js: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { io } from "socket.io-client"; 3 | 4 | export const state = reactive({ 5 | connected: false, 6 | }); 7 | 8 | const URL = process.env.NODE_ENV === "production" ? undefined : "http://localhost:2023"; 9 | 10 | export const socket = io(URL); 11 | 12 | socket.on("connect", () => { 13 | state.connected = true; 14 | }); 15 | 16 | socket.on("disconnect", () => { 17 | state.connected = false; 18 | }); -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | package-lock.json 12 | .DS_Store 13 | dist 14 | dist-ssr 15 | coverage 16 | *.local 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@creativebulma/bulma-tooltip": "^1.2.0", 12 | "axios": "^1.4.0", 13 | "bulma": "^0.9.4", 14 | "chart.js": "^4.3.2", 15 | "socket.io-client": "^4.7.1", 16 | "vue": "^3.3.4", 17 | "vue-chartjs": "^5.2.0" 18 | }, 19 | "devDependencies": { 20 | "@iconify/vue": "^4.1.1", 21 | "@vitejs/plugin-vue": "^4.2.3", 22 | "sass": "^1.64.1", 23 | "vite": "^4.4.6" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/user-manual/dashboard/dashboard.md: -------------------------------------------------------------------------------- 1 | ![Fast Dashboard](../../assets/images/dashboard.png) 2 | 3 | The dashboard provides real-time insights into exploits' performance during the current tick, reflects the state of the flags in the database, and displays the number of accepted flags for each exploit over the last ten ticks. 4 | 5 | Status icons portray the condition of each exploit, with a distinct "bug" icon signaling when an exploit impacts one of your own services, alerting the team to patch the service. 6 | 7 | Two countdowns tell the time remaining until the next tick and the next flag submission, along with the progress bar on top that represents the progression of the current tick. -------------------------------------------------------------------------------- /docs/user-manual/dashboard/overview.md: -------------------------------------------------------------------------------- 1 | # Fast Dashboard Overview 2 | 3 | ![Fast Dashboard](../../assets/images/dashboard.png) 4 | 5 | Fast comes with an intuitive web interface for monitoring and evaulating the performance of all running exploits during an A/D competition. You can track your exploits' performance in real-time, browse the flags using a simple yet flexible query language, and submit new flags manually. 6 | 7 | To access the dashboard, navigate to `http://:/` in your web browser. If Basic Auth is enabled, the browser will prompt you for the credentials (username can be set to anything). 8 | 9 | The interface consists of three views: 10 | 11 | - [Dashboard](dashboard.md) 12 | - [Flag Browser](browser.md) 13 | - [Manual Submitter](manual.md) 14 | -------------------------------------------------------------------------------- /docs/user-manual/dashboard/manual.md: -------------------------------------------------------------------------------- 1 | ![Manual Submitter](../../assets/images/manual.png) 2 | 3 | The Manual Submitter serves as a tool for submitting flags manually when you have the flags but not the exploit ready yet. It provides a text area where you can paste any content, such as HTTP response data containing one or multiple flags. After you paste the text, the flags will be matched using regex. 4 | 5 | The manual submitter offers two actions for handling matched flags: 6 | 7 | - **Queue the flags**: You can push the flags to the submission queue and wait for them to be submitted in the next scheduled submit. 8 | - **Submit immediately**: Alternatively, you can submit right away and get immediate results. 9 | 10 | The actual flag submission is handled by your submitter module. You can also manually submit dummy flags to ensure that the submitter module is functioning properly. 11 | -------------------------------------------------------------------------------- /util/styler.py: -------------------------------------------------------------------------------- 1 | class TextStyler: 2 | COLORS = { 3 | 'black': '30', 4 | 'red': '31', 5 | 'green': '32', 6 | 'yellow': '33', 7 | 'blue': '34', 8 | 'magenta': '35', 9 | 'cyan': '36', 10 | 'white': '37' 11 | } 12 | 13 | @staticmethod 14 | def bold(text): 15 | return f"\033[1m{text}\033[0m" 16 | 17 | @staticmethod 18 | def faint(text): 19 | return f"\033[2m{text}\033[0m" 20 | 21 | @staticmethod 22 | def italic(text): 23 | return f"\033[3m{text}\033[0m" 24 | 25 | @staticmethod 26 | def underline(text): 27 | return f"\033[4m{text}\033[0m" 28 | 29 | @staticmethod 30 | def color(text, color): 31 | color_code = TextStyler.COLORS.get(color.lower(), '39') # Default to '39' (default color) 32 | return f"\033[{color_code}m{text}\033[0m" 33 | -------------------------------------------------------------------------------- /util/helpers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from playhouse.shortcuts import model_to_dict 3 | 4 | 5 | def truncate(string, length): 6 | if length < 4: 7 | raise AttributeError("Max length must be at least 4 characters.") 8 | 9 | if string and len(string) > length: 10 | return string[:length - 3] + "..." 11 | return string 12 | 13 | 14 | def seconds_from_now(seconds): 15 | return datetime.now() + timedelta(seconds=seconds) 16 | 17 | 18 | def deep_update(left, right): 19 | """ 20 | Update a dictionary recursively in-place. 21 | """ 22 | for key, value in right.items(): 23 | if isinstance(value, dict) and value: 24 | returned = deep_update(left.get(key, {}), value) 25 | left[key] = returned 26 | else: 27 | left[key] = right[key] 28 | return left 29 | 30 | 31 | def flag_model_to_dict(instance): 32 | flag_dict = model_to_dict(instance) 33 | flag_dict['timestamp'] = instance.timestamp.isoformat() 34 | 35 | return flag_dict 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="fast", 5 | version="1.0.0", 6 | description="Flag Acquisition and Submission Tool ─ Easily manage your exploits in A/D competitions", 7 | author="Dušan Lazić", 8 | author_email="lazicdusan1104@gmail.com", 9 | url="https://github.com/dusanlazic/fast", 10 | install_requires=[ 11 | 'requests', 12 | 'loguru', 13 | 'pyyaml', 14 | 'peewee', 15 | 'psycopg2-binary', 16 | 'pyparsing', 17 | 'flask', 18 | 'flask_httpauth', 19 | 'flask_socketio', 20 | 'flask_cors', 21 | 'jsonschema', 22 | 'APScheduler==3.10.1', 23 | 'gevent-websocket', 24 | ], 25 | packages=['util', 'web'], 26 | package_data={'web': ['dist/*', 'dist/assets/*']}, 27 | py_modules=['cli', 'client', 'database', 'dsl', 'handler', 'models', 'runner', 'server'], 28 | entry_points= { 29 | 'console_scripts': [ 30 | 'fast = client:main', 31 | 'server = server:main', 32 | 'reset = cli:reset', 33 | 'fire = cli:fire', 34 | 'submit = cli:submit' 35 | ], 36 | } 37 | ) -------------------------------------------------------------------------------- /web/src/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const client = axios.create({ 4 | baseURL: process.env.NODE_ENV === 'production' ? undefined : 'http://localhost:2023' 5 | }) 6 | 7 | export default { 8 | async getTimersData() { 9 | const response = await client.get('/sync') 10 | return response.data 11 | }, 12 | async getFlagStoreStats() { 13 | const response = await client.get('/flagstore-stats') 14 | return response.data 15 | }, 16 | async getExploitAnalytics() { 17 | const response = await client.get('/exploit-analytics') 18 | return response.data 19 | }, 20 | async getFlagFormat() { 21 | const response = await client.get('/flag-format') 22 | return response.data 23 | }, 24 | async searchFlags(page, show, sort, query) { 25 | return await client.post('/search', { 26 | "page": page, 27 | "show": show, 28 | "sort": sort, 29 | "query": query 30 | }).then(function (response) { 31 | return response.data 32 | }).catch(function (error) { 33 | return error.response.data 34 | }) 35 | }, 36 | async submitFlags(flags, action, player) { 37 | const response = await client.post('/enqueue-manual', { 38 | "flags": flags, 39 | "action": action, 40 | "player": player 41 | }) 42 | return response.data 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /util/log.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import traceback 4 | from datetime import datetime 5 | from loguru import logger 6 | from util.styler import TextStyler as st 7 | 8 | LOG_DIR = 'logs' 9 | 10 | config = { 11 | "handlers": [ 12 | {"sink": sys.stdout, 13 | "format": "{time:HH:mm:ss} {level: >8} | {message}"}, 14 | ], 15 | } 16 | 17 | logger.configure(**config) 18 | 19 | 20 | def log_error(exploit_name, target, e): 21 | log_filename = get_log_filename(exploit_name, target) 22 | 23 | with open(log_filename, 'w') as error_output: 24 | traceback.print_exc(file=error_output) 25 | logger.info(st.faint(f"Error log saved in {log_filename}")) 26 | 27 | 28 | def log_warning(exploit_name, target, response): 29 | log_filename = get_log_filename(exploit_name, target) 30 | 31 | with open(log_filename, 'w') as error_output: 32 | error_output.write(response) 33 | logger.info(st.faint(f"Response saved in {log_filename}")) 34 | 35 | 36 | def create_log_dir(): 37 | if not os.path.exists(LOG_DIR): 38 | os.makedirs(LOG_DIR) 39 | logger.success(f'Created directory for error logs.') 40 | 41 | 42 | def get_log_filename(exploit_name, target): 43 | return os.path.join( 44 | LOG_DIR, f'{exploit_name}_{target}_{datetime.now().strftime("%H_%M_%S")}.txt') 45 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## Addressing limited number of VPN connections 2 | 3 | If you are running the Fast server on a Virtual Private Server (VPS), you might run into issues with reaching the competition infrastructure, such as the flag-checking service essential for flag submission. Event organizers may restrict the number of VPN connections to match the team size, leaving no room for connecting another machine such as your VPS. 4 | 5 | To address this, you can establish an SSH tunnel, enabling the VPS to route traffic to the flag-checking service using your local machine as a relay. 6 | 7 | ```bash 8 | ssh -i /path/to/your/ssh-key.pem -L 5000:10.10.13.37:1337 admin@fast.example.com 9 | ``` 10 | 11 | === "Direct Access" 12 | 13 | ```python 14 | r = remote('10.10.13.37', 1337) 15 | 16 | requests.post('http://10.10.13.37:1337/flags', json=flags) 17 | ``` 18 | 19 | === "Via SSH Tunnel" 20 | 21 | ```python 22 | r = remote('localhost', 5000) 23 | 24 | requests.post('http://localhost:5000/flags', json=flags) 25 | ``` 26 | 27 | In the example scenario above, the flag-checking service is available over the VPN at `10.10.13.37:1337`. 28 | 29 | By setting up an SSH tunnel, traffic directed to `localhost:5000` on the VPS is forwarded to `10.10.13.37:1337`. This tunneling ensures bidirectional communication, allowing both sending flags and receiving the responses. -------------------------------------------------------------------------------- /docs/user-manual/client/running.md: -------------------------------------------------------------------------------- 1 | # Running Fast Client 2 | 3 | 4 | 5 | 6 | Before running Fast client, ensure the following: 7 | 8 | - [x] **The server is up**: You or someone on your team has configured and started the server. 9 | - [x] **You can access the dashboard**: Open the dashboard to confirm your machine can access the Fast server. Ask your teammates for host, port, and password. 10 | - [x] **Client is configured**: You have [configured the client](configuration.md) and the configuration file (`fast.yaml`) is located in your current working directory. 11 | 12 | To run the client, just run the command `fast`. 13 | 14 |
15 | 22 | 23 | Fast client will start, connect and synchronize with the server. It will wait for the next tick to begin before it starts running your exploits. You can open the dashboard to monitor the tick clock and wait for your flags to show up. 24 | 25 | --- 26 | 27 | That's it. Good luck and have fun hacking! 🍀 -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from peewee import CharField, DateTimeField, IntegerField, Check 3 | from datetime import datetime 4 | from database import BaseModel, FallbackBaseModel 5 | 6 | class Flag(BaseModel): 7 | value = CharField(unique=True) 8 | exploit = CharField() 9 | player = CharField() 10 | tick = IntegerField() 11 | target = CharField() 12 | timestamp = DateTimeField(default=datetime.now) 13 | status = CharField(constraints=[Check("status IN ('queued', 'accepted', 'rejected')")]) 14 | response = CharField(null=True) 15 | 16 | 17 | class FallbackFlag(FallbackBaseModel): 18 | value = CharField() 19 | exploit = CharField() 20 | target = CharField() 21 | timestamp = DateTimeField(default=datetime.now) 22 | status = CharField(constraints=[Check("status IN ('pending', 'forwarded')")]) 23 | 24 | 25 | Batching = namedtuple('Batching', 'count size wait') 26 | 27 | class ExploitDetails: 28 | def __init__(self, name, targets, module=None, run=None, prepare=None, cleanup=None, timeout=None, env=None, delay=None, batching=None): 29 | self.name = name 30 | self.targets = targets 31 | self.module = module 32 | self.run = run 33 | self.prepare = prepare 34 | self.cleanup = cleanup 35 | self.timeout = timeout 36 | self.env = env 37 | self.delay = delay 38 | self.batching = batching 39 | -------------------------------------------------------------------------------- /docs/user-manual/server/running.md: -------------------------------------------------------------------------------- 1 | # Running Fast Server 2 | 3 | 4 | 5 | 6 | Before running Fast server, ensure the following: 7 | 8 | - [x] **Server is configured**: You have [configured the server](../server/configuration.md) and the configuration file (`server.yaml`) is located in your current working directory. 9 | - [x] **Submitter module is written**: You have written the submitter module according to the [Submitter Guideline](submitter-guideline.md) and it's located in your current working directory. 10 | 11 | To run the server, just run the command `server`. 12 | 13 |
14 | 21 | 22 | The Fast server is now ready to receive and submit flags. 23 | 24 | To access the dashboard, navigate to [http://localhost:2023](http://localhost:2023) in your web browser. Your teammates will have to use your machine's IP, which may be the one on your team's local network or the public IP if you are running on a VPS. Verify with your teammates that they can access the dashboard with no issues. 25 | -------------------------------------------------------------------------------- /docs/assets/demos/server-no-clients.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 125, "height": 24, "timestamp": 1692462036, "env": {"SHELL": "/bin/bash", "TERM": "screen-256color"}} 2 | [0.0, "o", "\u001b[01;32ms4ndu@server\u001b[00m:\u001b[01;34m~/CTF/FastDemo\u001b[00m$ "] 3 | [1.379226, "o", "s"] 4 | [1.451169, "o", "e"] 5 | [1.555049, "o", "r"] 6 | [1.803619, "o", "v"] 7 | [1.931607, "o", "e"] 8 | [1.995172, "o", "r"] 9 | [2.347078, "o", "\r\n"] 10 | [3.539305, "o", "\r\n\u001b[32;1m .___ ____\u001b[0m ______ __ \r\n\u001b[32;1m / /\\__/ /\u001b[0m / ____/_ ____ / /_ \r\n\u001b[32;1m / / / ❬` \u001b[0m / /_/ __ `/ ___/ __/\r\n\u001b[32;1m /___/ /____\\ \u001b[0m / __/ /_/ (__ ) /_ \r\n\u001b[32;1m / \\___\\/ \u001b[0m/_/ \\__,_/____/\\__/ \r\n\u001b[32;1m/\u001b[0m \u001b[32mserver\u001b[0m \u001b[2mv1.0.0\u001b[0m\r\n\r\n"] 11 | [3.570494, "o", "\u001b[2m18:20:40\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m Fast server configured successfully.\r\n"] 12 | [3.570739, "o", "\u001b[2m18:20:40\u001b[0m \u001b[1m INFO |\u001b[0m Server will run at \u001b[36mhttp://0.0.0.0:2023\u001b[0m.\r\n"] 13 | [3.612627, "o", "\u001b[2m18:20:40\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m Database connected.\r\n"] 14 | [4.1156, "o", "\u001b[2m18:20:40\u001b[0m \u001b[1m INFO |\u001b[0m Started tick \u001b[1m0\u001b[0m. Next tick scheduled for \u001b[1m18:21:00\u001b[0m. ⏱️\r\n"] 15 | [14.144199, "o", "\u001b[2m18:20:50\u001b[0m \u001b[1m INFO |\u001b[0m No flags in the queue! Submission skipped.\r\n"] -------------------------------------------------------------------------------- /web/src/timers.js: -------------------------------------------------------------------------------- 1 | import api from '@/api.js' 2 | import { socket } from "@/socket"; 3 | import { reactive, computed } from 'vue' 4 | 5 | socket.on('tickStart', function(msg) { 6 | timers.tick.number = msg.current 7 | }) 8 | 9 | export const timers = reactive({ 10 | tick: { 11 | number: 0, 12 | start: 0, 13 | duration: 0, 14 | elapsed: 0 15 | }, 16 | submitter: { 17 | start: 0, 18 | delay: 0, 19 | elapsed: 0 20 | }, 21 | async initialize() { 22 | const syncData = await api.getTimersData(); 23 | 24 | this.tick.duration = syncData.tick.duration * 1000 25 | this.tick.elapsed = syncData.tick.elapsed * 1000 26 | this.submitter.delay = syncData.submitter.delay * 1000 27 | 28 | this.tick.start = performance.now() - this.tick.elapsed 29 | this.submitter.start = this.tick.start + this.submitter.delay 30 | 31 | if (this.tick.elapsed < this.submitter.delay) { 32 | this.submitter.start -= this.tick.duration 33 | } 34 | 35 | this.updateTimers() 36 | }, 37 | async updateTimers() { 38 | this.tick.elapsed = (performance.now() - this.tick.start) % this.tick.duration 39 | this.submitter.elapsed = (performance.now() - this.submitter.start) % this.tick.duration 40 | 41 | requestAnimationFrame(() => this.updateTimers()) 42 | }, 43 | tickSecondsRemaining: computed(() => 44 | Math.ceil((timers.tick.duration - timers.tick.elapsed) / 1000) 45 | ), 46 | submitSecondsRemaining: computed(() => 47 | Math.ceil((timers.tick.duration - timers.submitter.elapsed) / 1000) 48 | ) 49 | }) -------------------------------------------------------------------------------- /docs/user-manual/server/overview.md: -------------------------------------------------------------------------------- 1 | # Fast Server Overview 2 | 3 | ``` mermaid 4 | graph LR 5 | 6 | targets["Opponents'
services"] 7 | targets --> aliceExploits[Alice's exploits] 8 | targets --> bobExploits[Bob's exploits] 9 | targets --> carolExploits[Carol's exploits] 10 | 11 | subgraph "Alice's machine" 12 | aliceExploits --> client1[Alice's client] 13 | end 14 | 15 | subgraph "Bob's machine" 16 | bobExploits --> client2[Bob's client] 17 | end 18 | 19 | subgraph "Carol's machine" 20 | carolExploits --> client3[Carol's client] 21 | end 22 | 23 | client1 -->|extracts &
forwards flags| server[Fast server] 24 | client2 -->|extracts &
forwards flags| server 25 | client3 -->|extracts &
forwards flags| server 26 | 27 | server -->|submits| flagService[Flag-checking service] 28 | ``` 29 | 30 | 31 | Fast server is responsible for collecting and submitting flags, filtering out duplicates, providing useful insights through the dashboard, and keeping all the connected clients in sync. It's designed to operate under heavy load conditions and implements measures for quick and easy recovery if anything goes wrong. 32 | 33 | This section provides instructions for [configuring the Fast server](configuration.md) and [writing the submitter](submitter-guideline.md). It includes real-world examples and details every option in the configuration YAML. You can use this documentation both as a guide during setup and as a reference in a competition. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 🚩 Fast — Flag Acquisition and Submission Tool 2 | 3 | !!! warning "Development Discontinued Notice" 4 | 5 | As of August 5th, 2024, this project will no longer be updated or supported as development shifts towards an undisclosed alternative for the Serbian National ECSC Team 🇷🇸. This repository will remain available for archival purposes. If you find this tool useful, feel free to adapt it as necessary. 6 | 7 | 8 | Fast is a specialized tool built in Python designed for managing exploits and automating flag submission in Attack/Defense (A/D) competitions. The goal of Fast is to take the technical burden off the team players, enabling them to focus on writing exploits and patching vulnerabilities. 9 | 10 | The development of Fast was heavily influenced by the practical experiences and valuable insights gathered by the **Serbian National ECSC Team** 🇷🇸 who utilized the tool in multiple A/D competitions. Fast's development roadmap will continue to be aligned with the team's needs. 11 | 12 | ## Using the Docs 13 | 14 | This documentation will assist you in getting started with Fast and will also serve as a reference during competitions. 15 | 16 | If this is your first time using Fast, you can get started with [Installation](install.md) and then proceed to [Quickstart](quickstart.md) to get familiar with the basics. To explore its features or seek help during a competition, refer to the three sections of the User Manual: [Server](user-manual/server/overview.md), [Dashboard](user-manual/dashboard/overview.md) and [Client](user-manual/client/overview.md). 17 | 18 | --- 19 | 20 | Good luck and have fun hacking! 🍀 21 | -------------------------------------------------------------------------------- /web/src/counters.js: -------------------------------------------------------------------------------- 1 | import api from '@/api.js' 2 | import { socket } from "@/socket"; 3 | import { reactive } from 'vue' 4 | 5 | 6 | socket.on('enqueue', function (msg) { 7 | counters.increment(msg) 8 | }) 9 | 10 | socket.on('enqueue_fallback', function (msg) { 11 | counters.increment_fallback(msg) 12 | }) 13 | 14 | socket.on('submitStart', function () { 15 | counters.store.submitting = true 16 | }) 17 | 18 | socket.on('submitComplete', function (msg) { 19 | counters.store.submitting = false 20 | counters.updateStoreStats(msg) 21 | }) 22 | 23 | socket.on('tickStart', function() { 24 | counters.resetTickCounters() 25 | }) 26 | 27 | export const counters = reactive({ 28 | tick: { 29 | received: 0, 30 | duplicates: 0, 31 | queued: 0, 32 | exploits: new Set() 33 | }, 34 | store: { 35 | queued: 0, 36 | accepted: 0, 37 | rejected: 0, 38 | delta: { 39 | accepted: 0, 40 | rejected: 0 41 | }, 42 | submitting: false 43 | }, 44 | async initialize() { 45 | const data = await api.getFlagStoreStats() 46 | this.store = data 47 | }, 48 | increment(data) { 49 | this.tick.exploits.add(`${data.player}/${data.exploit}`) 50 | this.tick.received += data.new + data.dup 51 | this.tick.duplicates += data.dup 52 | this.tick.queued += data.new 53 | this.store.queued += data.new 54 | }, 55 | increment_fallback(data) { 56 | this.tick.received += data.new + data.dup 57 | this.tick.duplicates += data.dup 58 | this.tick.queued += data.new 59 | this.store.queued += data.new 60 | }, 61 | updateStoreStats(data) { 62 | this.store = data.data 63 | }, 64 | resetTickCounters() { 65 | this.tick.received = 0 66 | this.tick.duplicates = 0 67 | this.tick.queued = 0 68 | this.tick.exploits.clear() 69 | } 70 | }) -------------------------------------------------------------------------------- /docs/assets/demos/client-no-exploits.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 125, "height": 24, "timestamp": 1692462535, "env": {"SHELL": "/bin/bash", "TERM": "screen-256color"}} 2 | [0.0, "o", "\u001b[01;32ms4ndu@player\u001b[00m:\u001b[01;34m~/CTF/FastDemo\u001b[00m$ "] 3 | [1.510916, "o", "f"] 4 | [1.584608, "o", "a"] 5 | [1.637811, "o", "s"] 6 | [1.750134, "o", "t"] 7 | [2.038081, "o", "\r\n"] 8 | [2.887801, "o", "\r\n\u001b[34;1m .___ ____\u001b[0m ______ __ \r\n\u001b[34;1m / /\\__/ /\u001b[0m / ____/_ ____ / /_ \r\n\u001b[34;1m / / / ❬` \u001b[0m / /_/ __ `/ ___/ __/\r\n\u001b[34;1m /___/ /____\\ \u001b[0m / __/ /_/ (__ ) /_ \r\n\u001b[34;1m / \\___\\/ \u001b[0m/_/ \\__,_/____/\\__/ \r\n\u001b[34;1m/\u001b[0m \u001b[34mclient\u001b[0m \u001b[2mv1.0.0\u001b[0m\r\n\r\n"] 9 | [2.889776, "o", "\u001b[2m18:28:58\u001b[0m \u001b[1m INFO |\u001b[0m Loading connection config...\r\n"] 10 | [2.896851, "o", "\u001b[2m18:28:58\u001b[0m \u001b[1m INFO |\u001b[0m Checking exploits config...\r\n"] 11 | [2.896987, "o", "\u001b[2m18:28:58\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1mexploits\u001b[0m section contains no exploits. Please add your exploits to start running them in the next tick.\r\n"] 12 | [2.897112, "o", "\u001b[2m18:28:58\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m No errors found in exploits config.\r\n"] 13 | [2.897242, "o", "\u001b[2m18:28:58\u001b[0m \u001b[1m INFO |\u001b[0m Fetching game config...\r\n"] 14 | [2.897413, "o", "\u001b[2m18:28:58\u001b[0m \u001b[1m INFO |\u001b[0m Connecting to \u001b[36mhttp://yourname@192.168.13.37:2023\u001b[0m\r\n"] 15 | [2.905618, "o", "\u001b[2m18:28:58\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m Game configured successfully. — \u001b[2m20s tick, DEMO[A-Za-z0-9+\\/=]{48}, 10.1.5.1\u001b[0m\r\n"] 16 | [2.911085, "o", "\u001b[2m18:28:58\u001b[0m \u001b[1m INFO |\u001b[0m Synchronizing with the server... Tick will start at \u001b[1m18:29:20\u001b[0m.\r\n"] -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Fast Docs 2 | site_url: https://lazicdusan.com/fast 3 | repo_url: https://github.com/dusanlazic/fast 4 | repo_name: dusanlazic/fast 5 | theme: 6 | name: material 7 | favicon: assets/favicon.ico 8 | logo: assets/flag-line.svg 9 | features: 10 | - content.code.copy 11 | - navigation.footer 12 | - navigation.instant 13 | palette: 14 | - scheme: slate 15 | toggle: 16 | icon: material/brightness-4 17 | name: Switch to light mode 18 | - scheme: default 19 | toggle: 20 | icon: material/brightness-7 21 | name: Switch to dark mode 22 | markdown_extensions: 23 | - attr_list 24 | - def_list 25 | - admonition 26 | - pymdownx.details 27 | - pymdownx.superfences: 28 | custom_fences: 29 | - name: mermaid 30 | class: mermaid 31 | format: !!python/name:pymdownx.superfences.fence_code_format 32 | - pymdownx.highlight: 33 | anchor_linenums: true 34 | - pymdownx.tabbed: 35 | alternate_style: true 36 | - pymdownx.tasklist: 37 | custom_checkbox: true 38 | - toc: 39 | permalink: true 40 | 41 | nav: 42 | - 'Introduction': 'index.md' 43 | - 'Installation': 'install.md' 44 | - 'Quickstart': 'quickstart.md' 45 | - 'User Manual': 46 | - 'Fast Server': 47 | - 'Overview': 'user-manual/server/overview.md' 48 | - 'Configuration': 'user-manual/server/configuration.md' 49 | - 'Submitter Guideline': 'user-manual/server/submitter-guideline.md' 50 | - 'Running': 'user-manual/server/running.md' 51 | - 'Fast Dashboard': 52 | - 'Overview': 'user-manual/dashboard/overview.md' 53 | - 'Dashboard': 'user-manual/dashboard/dashboard.md' 54 | - 'Flag Browser': 'user-manual/dashboard/browser.md' 55 | - 'Manual Submitter': 'user-manual/dashboard/manual.md' 56 | - 'Fast Client': 57 | - 'Overview': 'user-manual/client/overview.md' 58 | - 'Configuration': 'user-manual/client/configuration.md' 59 | - 'Exploit Guideline': 'user-manual/client/exploit-guideline.md' 60 | - 'Exploit Management': 'user-manual/client/exploit-management.md' 61 | - 'Running': 'user-manual/client/running.md' 62 | - 'Troubleshooting': 'troubleshooting.md' -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 15 | 16 | 69 | -------------------------------------------------------------------------------- /web/src/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 70 | 71 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | Installing Fast is a straightforward process with no need to clone the repo or manually build the frontend. You can install it in your environment using a single pip command. 2 | 3 | ## Installing via pip recommended { #installing-via-pip data-toc-label="Installing via pip" } 4 | 5 | To install the latest release, run the following command. 6 | 7 | === "Linux" 8 | 9 | ```sh 10 | pip install https://github.com/dusanlazic/fast/releases/download/v1.0.0/fast-1.0.0.tar.gz 11 | ``` 12 | 13 | === "Windows" 14 | 15 | ```powershell 16 | pip install https://github.com/dusanlazic/fast/releases/download/v1.0.0/fast-1.0.0.zip 17 | ``` 18 | 19 | Older versions can be found on the [releases page](https://github.com/dusanlazic/fast/releases) on GitHub. 20 | 21 | !!! tip 22 | 23 | It's highly recommended to install Fast within a Python virtual environment. This isolates the dependencies and ensures a clean workspace. To create and activate a new Python virtual environment, run the following command: 24 | 25 | === "Linux" 26 | 27 | ```sh 28 | python3 -m venv venv && source venv/bin/activate 29 | ``` 30 | 31 | === "Windows" 32 | 33 | ```powershell 34 | python -m venv venv && .\venv\Scripts\activate 35 | ``` 36 | 37 | ## Installing From Source 38 | 39 | Fast can also be installed directly from its source. This requires building the frontend with `npm`. 40 | 41 | ```sh 42 | git clone https://github.com/dusanlazic/fast.git 43 | cd fast/web/ 44 | npm install 45 | npm run build 46 | cd ../../ 47 | pip install -e fast/ 48 | ``` 49 | 50 | ## Next Steps 51 | 52 | Once the installation is complete, two main commands will be accessible from *any* directory on your system: 53 | 54 | - `fast`: For running the client, allowing you to manage and run exploits. 55 | - `server`: For running the server, used for flag submission and other server-related tasks. 56 | 57 | Before running the client or the server, you will need to configure them using YAML files. Fast will always look for the configuration and other relevant files within the current working directory, allowing you to have multiple separate configurations for different competitions. 58 | 59 | If you are already familiar with the tool, you can move on to [Client Configuration](user-manual/client/configuration.md) if you want to run and manage exploits, or [Server Configuration](user-manual/server/configuration.md) to configure the server. 60 | 61 | To get familiar with the basics and get Fast running quickly, continue to [Quickstart](quickstart.md). -------------------------------------------------------------------------------- /docs/user-manual/client/overview.md: -------------------------------------------------------------------------------- 1 | # Fast Client Overview 2 | 3 | ``` mermaid 4 | graph LR 5 | 6 | client[Fast client] -->|spawns
subprocess| alpha["runner"] 7 | client[Fast client] -->|spawns
subprocess| bravo["runner"] 8 | 9 | subgraph "Exploit Alpha" 10 | alpha -->|spawns
thread| alphaThread1["exploit(10.1.2.1)"] 11 | alpha -->|spawns
thread| alphaThread2["exploit(10.1.3.1)"] 12 | alpha -->|spawns
thread| alphaThread3["exploit(10.1.4.1)"] 13 | end 14 | 15 | subgraph "Exploit Bravo" 16 | bravo -->|spawns
thread| bravoThread1["exploit(10.1.2.1)"] 17 | bravo -->|spawns
thread| bravoThread2["exploit(10.1.3.1)"] 18 | bravo -->|spawns
thread| bravoThread3["exploit(10.1.4.1)"] 19 | end 20 | 21 | subgraph "Opponents'
  services" 22 | target1[10.1.2.1] 23 | target2[10.1.3.1] 24 | target3[10.1.4.1] 25 | end 26 | 27 | alphaThread1 -->|attacks| target1 28 | alphaThread2 -->|attacks| target2 29 | alphaThread3 -->|attacks| target3 30 | 31 | bravoThread1 -->|attacks| target1 32 | bravoThread2 -->|attacks| target2 33 | bravoThread3 -->|attacks| target3 34 | ``` 35 | 36 | Anyone on the team can run a separate Fast client on their own machine. Each client's primary role is to run the exploits according to the user's specification. Fast provides an intuitive way of configuring using a single YAML file. That includes specifying targets, customizing each exploit's environment, and strategic arrangement of the attacks to ensure optimal CPU, memory and network resource utilization. 37 | 38 | The core functionality of Fast is to run the attacks concurrently and independently of each other. If an exploit crashes or timeouts on one target, its execution on the other targets will remain unaffected. This is achieved by using threading library, spawning a separate Python thread for each target. 39 | 40 | Exploits are ran separately as subprocesses. This lets attacks of the same exploit share the same Python interpreter, enabling the use of `prepare` and `cleanup` functions in your exploit code, as well as bypassing the main process's Global Interpreter Lock (GIL). 41 | 42 | Additionally, all clients will conform to the flag format specified on the server and synchronize with the server's tick clock, making them remain in sync without any time drifts or offsets. 43 | 44 | This documentation section covers [client configuration](configuration.md), [exploit development guideline](exploit-guideline.md), and [exploit management](exploit-management.md). Real-world examples complement each topic. You can use this documentation both as a guide during setup and as a reference in a competition. -------------------------------------------------------------------------------- /web/src/components/ManualSubmit.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 80 | -------------------------------------------------------------------------------- /docs/user-manual/server/submitter-guideline.md: -------------------------------------------------------------------------------- 1 | The submitter module is a vital part of Fast, responsible for the actual submission of flags to the competition's flag-checking service. Fast is adaptable to any A/D competition because it lets you write this module yourself. To work properly with Fast, your submitter script must follow the simple guideline specified below. 2 | 3 | ## Structure of the Submitter Module 4 | 5 | The submitter script must define a function named `submit` that takes a list of flags (as string values) ready for submission. This function is responsible for submitting the flags to the flag-checking service and collecting the responses. 6 | 7 | The `submit` function returns a tuple of two dictionaries: 8 | 9 | 1. **Accepted Flags**: A dictionary containing the flags that were accepted by the service, with the flag as the key and the corresponding response as the value. 10 | 2. **Rejected Flags**: A dictionary containing the flags that were rejected by the service, with the flag as the key and the corresponding response as the value. 11 | 12 | You can adapt the submit function to work with various flag submission mechanisms, such as submitting through a REST API, or over a raw TCP connection. See below for examples. 13 | 14 | === "HTTP" 15 | 16 | ```python 17 | import requests 18 | 19 | def submit(flags): 20 | flag_responses = requests.post('http://example.ctf/flags', json=flags).json() 21 | accepted_flags = { item['flag']: item['response'] for item in flag_responses if item['response'].endswith('OK') } 22 | rejected_flags = { item['flag']: item['response'] for item in flag_responses if not item['response'].endswith('OK') } 23 | return accepted_flags, rejected_flags 24 | ``` 25 | 26 | === "Raw TCP" 27 | 28 | ```python 29 | from pwn import * 30 | 31 | def submit(flags): 32 | accepted_flags, rejected_flags = {} 33 | r = remote('flags.example.ctf', 1337) 34 | for flag in flags: 35 | r.sendline(flag.encode()) 36 | response = r.recvline().decode().strip() 37 | if response.endswith('OK') 38 | accepted_flags[flag] = response 39 | else: 40 | rejected_flags[flag] = response 41 | return accepted_flags, rejected_flags 42 | ``` 43 | 44 | 45 | ## Template 46 | 47 | If you like type hints, you can use the following template. 48 | 49 | ```python 50 | from typing import List, Tuple, Dict 51 | 52 | def submit(flags: List[str]) -> Tuple[Dict[str, str], Dict[str, str]]: 53 | accepted_flags = {} 54 | rejected_flags = {} 55 | 56 | # Submit and categorize flags 57 | 58 | return accepted_flags, rejected_flags 59 | 60 | ``` 61 | 62 | ## Integration 63 | 64 | The script must be placed in the same directory as your `server.yaml` configuration file. By default, it must be named `submitter.py`. 65 | 66 | If you need to name it differently, see the example in the [User Manual](configuration.md#examples_1). -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey 2 | monkey.patch_all() 3 | import os 4 | import argparse 5 | import threading 6 | from util.log import logger 7 | from util.styler import TextStyler as st 8 | from client import load_exploits, load_config, setup_handler, run_exploit 9 | from server import setup_database 10 | from database import db 11 | from models import Flag 12 | from handler import SubmitClient 13 | 14 | 15 | def fire(): 16 | parser = argparse.ArgumentParser( 17 | description="Run exploits manually. Useful if you do not want to wait for the next tick.") 18 | parser.add_argument("names", metavar="Name", type=str, nargs="+", 19 | help="Names of the exploits as in fast.yaml") 20 | args = parser.parse_args() 21 | 22 | load_config() 23 | setup_handler(fire_mode=True) 24 | exploits = load_exploits() 25 | 26 | selected_exploits = [e for e in exploits if e.name in args.names] 27 | 28 | invalid_names = [n for n in args.names if n not in [ 29 | e.name for e in selected_exploits]] 30 | if invalid_names: 31 | logger.error( 32 | f"Exploits with the following names not found: {st.bold(', '.join(invalid_names))}") 33 | logger.info( 34 | f"Available exploits: {st.bold(', '.join([e.name for e in exploits]))}") 35 | 36 | for exploit in selected_exploits: 37 | exploit.delay = 0 # Ignore delay and run instantly 38 | threading.Thread(target=run_exploit, args=(exploit,)).start() 39 | 40 | 41 | def submit(): 42 | logger.info("Submission triggered...") 43 | client = SubmitClient() 44 | client.trigger_submit() 45 | stats = client.get_flagstore_stats() 46 | logger.info(f"{st.bold('Stats')} — {st.bold(stats['queued'])} queued, {st.bold(stats['accepted'])} accepted, {st.bold(stats['rejected'])} rejected.") 47 | 48 | 49 | def reset(): 50 | RECOVERY_CONFIG_PATH = '.recover.json' 51 | QMARK = st.color('?', 'cyan') 52 | PLUS = st.color('OK', 'green') 53 | INFO = st.faint('SKIP') 54 | 55 | if os.path.isfile(RECOVERY_CONFIG_PATH): 56 | print(f"{QMARK} Do you want to {st.color('reset the tick clock', 'red')}?") 57 | confirm_string = 'reset' 58 | confirmation = input(f" Type {st.color(confirm_string, 'cyan')} to confirm. ") == confirm_string 59 | if confirmation: 60 | os.remove(RECOVERY_CONFIG_PATH) 61 | print(f"{PLUS} Tick clock cleared.") 62 | else: 63 | print(f"{INFO} Tick clock left intact.") 64 | 65 | setup_database(log=False) 66 | print(f"{QMARK} Do you want to {st.color('delete the existing flags', 'red')}?") 67 | confirm_string = ');drop table flags;--' 68 | confirmation = input(f' Type {st.color(confirm_string, "cyan")} to delete all the previous flags.\n > ') == confirm_string 69 | 70 | if confirmation: 71 | db.drop_tables([Flag]) 72 | print(f"{PLUS} Table 'Flag' dropped.") 73 | else: 74 | print(f"{INFO} Flags left intact.") 75 | -------------------------------------------------------------------------------- /docs/user-manual/dashboard/browser.md: -------------------------------------------------------------------------------- 1 | ![Flag Browser](../../assets/images/browser.png) 2 | 3 | The flag browser offers a way to search for flags using a simple, user-friendly, and flexible **query language**. Being able to efficiently browse flags enchances transparency and allows various useful insights, such as checking how your newly added exploit behaves, identifying affected targets, discovering problems early, and many more. 4 | 5 | ## Query Language 6 | 7 | The query language provides a powerful way to filter and search flags based on various criteria. It is designed to be flexible and intuitive, using symbols and keywords that resemble both natural language and common programming syntax. 8 | 9 | Below you will find a comprehensive list of symbols and keywords, along with examples. These are provided as an aid for using the query language. It's recommended to play around with the flag browser to become familiar with its functionality, and refer to this guide when needed. 10 | 11 | !!! note 12 | 13 | All keywords are **case-insensitive**. 14 | 15 | ### Comparison Operators 16 | 17 | ``` 18 | player is s4ndu 19 | target = "10.1.3.1" 20 | tick >= 30 21 | timestamp after 16:00 22 | tick between 5 and 30 23 | tick between [5, 30] 24 | timestamp between 16:00 and 16:10 25 | ``` 26 | 27 | - Equal to (`==`, `=`, `equals`, `eq`, `is`) 28 | - Not equal to (`!=`, `<>`, `not equals`, `ne`, `is not`) 29 | - Greater than (`>`, `gt`, `over`, `above`, `greater than`) 30 | - Less than (`<`, `lt`, `under`, `below`, `less than`) 31 | - Greater or equal to (`>=`, `ge`, `min`, `not under`, `not below`, `after`) 32 | - Less or equal to (`<=`, `le`, `max`, `not over`, `not above`, `before`) 33 | - Between (`between`) 34 | 35 | ### String Matching 36 | 37 | ``` 38 | response matches ".* OK" 39 | target contains 10.10. 40 | value starts with "FAST{" 41 | response ending with OK 42 | ``` 43 | 44 | - Matches regex pattern (`matches`, `matching`, `regex`) 45 | - Contains substring (`contains`, `containing`) 46 | - Starts with substring (`starts with`, `starting with`, `begins with`, `beginning with`) 47 | - Ends with substring (`ends with`, `ending with`) 48 | 49 | ### In and Not In 50 | 51 | ``` 52 | status in [accepted, rejected] 53 | exploit not in [alpha, bravo] 54 | player of [alice, bob] 55 | player not of ['alice', 'bob'] 56 | ``` 57 | 58 | - Contained in (`in`, `of`) 59 | - Not contained in (`not in`, `not of`) 60 | 61 | ### Describing Timestamps 62 | 63 | !!! note 64 | 65 | The today's date is assumed when searching by timestamp. 66 | 67 | ``` 68 | 16:00, 16.00, 16-00, 16:00:45, 09:20, 9:20 69 | 5 mins ago, 1 hour ago, 5 hour ago, 20 seconds ago 70 | ``` 71 | 72 | - Seconds ago (`s`, `sec`, `second`, `seconds`) + `ago` 73 | - Minutes ago (`m`, `min`, `mins`, `minute`, `minutes`) + `ago` 74 | - Hours ago (`h`, `hour`, `hours`) + `ago` 75 | 76 | ### Logical Operators 77 | 78 | 79 | You can combine multiple search criteria using logical operators, with a precedence order of "`NOT`, `AND`, `OR`." Parentheses can be used to override this precedence and can be nested, allowing highly customizable queries. 80 | 81 | ``` 82 | player is s4ndu and exploit is alpha 83 | status == accepted && (exploit == alpha || target == 10.1.4.1) 84 | exploit is alpha or (target is 10.1.4.1 and (timestamp < 16:00 or timestamp > 18:00)) 85 | ``` 86 | 87 | - Conjunction (`and`, `&`, `&&`, `,`) 88 | - Disjunction (`or`, `|`, `||`) 89 | - Negation (`not`, `~`, `!`) 90 | -------------------------------------------------------------------------------- /docs/user-manual/client/configuration.md: -------------------------------------------------------------------------------- 1 | # Fast Client Configuration 2 | 3 | 4 | 5 | 6 | Client configuration is managed using a YAML file named `fast.yaml`. Fast looks for the configuration file and exploit scripts in the current working directory. This allows having multiple separate configurations and environments for different competitions. 7 | 8 | The `fast.yaml` file is composed of two sections, one for configuring the connection with the server, and one for exploit management. This page focuses on the first section. If you are looking for exploit management, see [Exploit Management](exploit-management.md) page. 9 | 10 | ## Connecting 11 | 12 | To setup Fast client for connecting to the server, you need to specify your server's **host**, **port**, **password** (if required), and your custom **username**. 13 | 14 | Some starter `fast.yaml` configurations with no managed exploits are shown below: 15 | 16 | ### Examples 17 | 18 | === "Minimum Team Config" 19 | 20 | ```yaml 21 | connect: 22 | host: 192.168.13.37 23 | player: john 24 | 25 | exploits: 26 | ``` 27 | 28 | 29 | === "Customized Port" 30 | 31 | ```yaml 32 | connect: 33 | host: 192.168.13.37 34 | port: 80 35 | player: john 36 | 37 | exploits: 38 | ``` 39 | 40 | === "Password Auth" 41 | 42 | ```yaml 43 | connect: 44 | host: 192.168.13.37 45 | password: Noflags4you! 46 | player: john 47 | 48 | exploits: 49 | ``` 50 | 51 | === "Public Server" 52 | 53 | ```yaml 54 | connect: 55 | host: fast.example.com 56 | password: Noflags4you! 57 | player: john 58 | 59 | exploits: 60 | ``` 61 | 62 | === "Single Player" 63 | 64 | ```yaml 65 | # Omit to connect to localhost:2023 with no password 66 | 67 | exploits: 68 | ``` 69 | 70 | Although the shown examples have no managed exploits, every given configuration is sufficient for launching the client. 71 | 72 | You can test the connection by running `fast` in your terminal from the same directory. If everything is OK, the client will start, synchronize with the server, and wait for exploits. 73 | 74 |
75 | 82 | 83 | ### Options 84 | 85 | This section starts with the keyword `connect:` placed anywhere at the root level of the file. 86 | 87 | `host` default = `localhost` 88 | : Host address of the Fast server. By default, client will connect to a Fast server running on localhost. 89 | 90 | `port` default = `2023` 91 | : Port number on which the Fast server is listening. Default is `2023`. 92 | 93 | `player` default = `anon` 94 | : Your name or alias to help Fast distinguish your exploits from those of your teammates. 95 | 96 | `password` default = `None` 97 | : Password for authenticating with the Fast server. 98 | 99 | ## Next Steps 100 | 101 | To learn how to start running and managing exploits, read the [Exploit Guideline](exploit-guideline.md) and then continue to [Exploit Management](exploit-management.md). -------------------------------------------------------------------------------- /docs/user-manual/client/exploit-guideline.md: -------------------------------------------------------------------------------- 1 | Fast is built around exploits, making them the central component when it comes to attacking the opponent teams. The "Flag Acquisition" part of Fast is done by the exploits written by you and your team. To work properly with Fast, exploit scripts must be placed in the same directory as `fast.yaml` and follow the simple guideline specified below. 2 | 3 | ## Python Exploits 4 | 5 | ### Structure 6 | 7 | A Python exploit script must define a function named `exploit`, taking the target's IP address as the sole parameter. This function is responsible for exploiting the service, and it returns a string containing one or multiple flags. That's about it, here's a minimal example: 8 | 9 | ```py 10 | import requests 11 | 12 | def exploit(target): 13 | return requests.get(f'http://{target}:1234/flag').text 14 | ``` 15 | 16 | ### Template 17 | 18 | If you like type hints, you can use the following template. 19 | 20 | ```python 21 | def exploit(target: str) -> str: 22 | 23 | # Exploit the target, get the flags 24 | 25 | return text_containing_flags 26 | ``` 27 | 28 | ### Prepare and Cleanup 29 | 30 | Some A/D competitions include an endpoint that provides you with additional pieces of information that may be necessary to exploit particular services. This typically includes something like the username of the account that can read the flag, some useful filename, etc. 31 | 32 | This data could be fetched only once and be reused for all the targets, saving both bandwidth and memory. For this kind of tasks, you can define the `prepare` function in your script, which will be invoked by Fast **once** before exploiting any of the targets. 33 | 34 | ```python 35 | import requests 36 | 37 | shared = {} 38 | 39 | def prepare(): 40 | attack_json = requests.get('https://example.ctf/attack.json').json() 41 | shared['attack_json'] = attack_json 42 | 43 | def exploit(target): 44 | username = shared['attack_json'][target]['example_service']['username'] 45 | 46 | return requests.get(f'http://{target}:1337/readflag?username={username}').text 47 | ``` 48 | 49 | The `prepare` function in the shown example fetches the data and stores it in a global variable, as a way of preparing the environment before exploiting the services. 50 | 51 | --- 52 | 53 | On the opposite end, you can define a function named `cleanup` for performing any needed post-exploitation actions. As the name suggests, it may be used for removing the residual files your exploit may create or download. 54 | 55 | ```python 56 | import os, glob 57 | 58 | # exploit and prepare omitted for brevity 59 | 60 | def cleanup(): 61 | for file in glob.glob('files/alpha/*.pdf'): 62 | os.remove(file) 63 | ``` 64 | 65 | The `cleanup` function in the shown example removes all PDF files downloaded by the exploit. 66 | 67 | !!! note 68 | Prepare and cleanup actions can also be defined as shell commands in `fast.yaml`. This way is less powerful since it provides no access to the shared interpreter, but it's compatible with non-Python exploits. Consult the [Exploit Management](exploit-management.md) page for more details. 69 | 70 | 71 | ## Non-Python Exploits 72 | 73 | ### Structure 74 | 75 | When it comes to non-Python scripts, ensure that the target's IP address can be passed as a command-line argument. The script should only output the text containing one or multiple flags to the standard output (stdout). Here's an example using a Bash script: 76 | 77 | ```bash 78 | #!/bin/bash 79 | curl -s "http://$1:1234/flag" 80 | ``` 81 | 82 | ## Next steps 83 | 84 | Once you have a new exploit ready, you can hand it to Fast to start running it. To learn how to manage exploits and see what capabilities Fast offers, continue to [Exploit Management](exploit-management.md). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚩 Fast — Flag Acquisition and Submission Tool 2 | 3 | 4 | > [!WARNING] 5 | > As of August 5th, 2024, this project will no longer be updated or supported as development shifts towards an undisclosed alternative for the Serbian National ECSC Team 🇷🇸. This repository will remain available for archival purposes. If you find this tool useful, feel free to adapt it as necessary. 6 | 7 | Fast is a specialized tool built in Python designed for managing exploits and automating flag submission in Attack/Defense (A/D) competitions. The goal of Fast is to take the technical burden off the team players, allowing them to focus on writing exploits and patching vulnerabilities. 8 | 9 | The development of Fast was heavily influenced by the practical experiences and valuable insights gathered by the **Serbian National ECSC Team** 🇷🇸 who utilized the tool in multiple A/D competitions. Fast's development roadmap will continue to be aligned with the team's needs. 10 | 11 | ## Installation 12 | 13 | To install the latest release, run the following command: 14 | 15 | ```sh 16 | pip install https://github.com/dusanlazic/fast/releases/download/v1.0.0/fast-1.0.0.tar.gz 17 | ``` 18 | 19 | ## Overview 20 | 21 | ### Manage Exploits with YAML 22 | 23 | ```yaml 24 | connect: 25 | host: 192.168.13.37 26 | port: 2023 27 | player: s4ndu 28 | 29 | exploits: 30 | - name: alpha 31 | targets: 32 | - 10.1.2-11.1 33 | 34 | - name: bravo 35 | targets: 36 | - 10.1.2.1 37 | - 10.1.6.1 38 | - 10.1.8-11.1 39 | 40 | - name: charlie 41 | run: ./charlie.sh [ip] 42 | targets: 43 | - 10.1.2-11.1 44 | ``` 45 | 46 | ### Utilize Tick Time Wisely 47 | 48 | ```yaml 49 | - name: lima 50 | batches: 51 | count: 5 52 | wait: 3 53 | targets: 54 | - 10.1.2-31.1 55 | 56 | - name: mike 57 | delay: 2 58 | batches: 59 | size: 8 60 | wait: 3 61 | targets: 62 | - 10.1.2-31.1 63 | ``` 64 | 65 | ![](docs/assets/images/attacks3.png) 66 | 67 | 68 | ### Straightforward Exploit and Submitter Templates 69 | 70 | 71 | ```python 72 | def exploit(target: str) -> str: 73 | 74 | # Exploit the target, get the flags 75 | 76 | return text_containing_flags 77 | ``` 78 | 79 | ```python 80 | from typing import List, Tuple, Dict 81 | 82 | def submit(flags: List[str]) -> Tuple[Dict[str, str], Dict[str, str]]: 83 | accepted_flags = {} 84 | rejected_flags = {} 85 | 86 | # Submit and categorize flags 87 | 88 | return accepted_flags, rejected_flags 89 | 90 | ``` 91 | 92 | ### See the Flags Yourself With Dashboard 93 | 94 | ![](docs/assets/images/dashboard.png) 95 | 96 | ![](docs/assets/images/browser.png) 97 | 98 | 99 | ### Pressure-Friendly Query Language 100 | 101 | Here are examples of some actual queries. Basically, anything you write will likely work. 102 | 103 | ``` 104 | player is alice 105 | target = 10.1.3.5 106 | tick >= 25 107 | timestamp after 15:30 108 | status is accepted 109 | response matches ".* OK" 110 | value starts with "FAST{" 111 | status in [accepted, rejected] 112 | exploit not in [alpha, bravo] 113 | timestamp between 14:00 and 16:00 114 | response ending with "OK" 115 | player is bob and status is rejected 116 | exploit is alpha or target = "10.1.4.1" 117 | status == accepted && (exploit == beta || target == 10.1.5.1) 118 | player is charlie and timestamp < 12:00 119 | value contains "FAST" and status is not rejected 120 | target starts with "10.1." 121 | response != "DEMOffoxfzhX0avVWS/wBb0oMljtFde6Ir/10GUmv3aXFIcUXbM= OLD" 122 | tick between [10, 20] 123 | player is not alice and status in [accepted, queued] 124 | exploit is delta and (timestamp > 10:00 && timestamp < 14:00) 125 | response matches ".* DUP" 126 | player of [eve, frank] 127 | target ends with ".5" 128 | exploit is gamma and status is not queued 129 | exploit is alpha or (target is 10.1.4.1 and (timestamp < 16:00 or timestamp > 18:00)) 130 | ``` 131 | 132 | ### Read the Docs for more 133 | 134 | [Fast docs](https://lazicdusan.com/fast) 135 | -------------------------------------------------------------------------------- /handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import requests 4 | from util.log import logger 5 | from util.styler import TextStyler as st 6 | from datetime import datetime, timedelta 7 | from requests.auth import HTTPBasicAuth 8 | from database import fallbackdb 9 | from models import FallbackFlag 10 | 11 | IMMUTABLE_CONFIG_PATH = '.config.json' 12 | 13 | headers = { 14 | 'Content-Type': 'application/json' 15 | } 16 | 17 | 18 | class SubmitClient(object): 19 | def __init__(self, connect=None): 20 | self.auth = None 21 | if connect: 22 | self.connect = connect 23 | self._client_configure() 24 | else: 25 | self._runner_configure() 26 | self._connect_to_fallbackdb() 27 | 28 | def _client_configure(self): 29 | self._update_url() 30 | self._update_auth() 31 | response = requests.get(f'{self.url}/config', params={'player': self.connect['player']}, auth=self.auth) 32 | response.raise_for_status() 33 | server_config = response.json() 34 | self.game = server_config['game'] 35 | 36 | immutable_config = { 37 | 'game': self.game, 38 | 'connect': self.connect 39 | } 40 | 41 | # Persist config so runners can reuse it. 42 | with open(IMMUTABLE_CONFIG_PATH, 'w') as file: 43 | file.write(json.dumps(immutable_config)) 44 | 45 | def _runner_configure(self): 46 | with open(IMMUTABLE_CONFIG_PATH) as file: 47 | immutable_config = json.loads(file.read()) 48 | 49 | self.game = immutable_config['game'] 50 | self.connect = immutable_config['connect'] 51 | self._update_url() 52 | self._update_auth() 53 | 54 | def _update_url(self): 55 | protocol = self.connect['protocol'] 56 | host = self.connect['host'] 57 | port = self.connect['port'] 58 | self.url = f"{protocol}://{host}:{port}" 59 | 60 | def _update_auth(self): 61 | if self.connect.get('password') != None: 62 | self.auth = HTTPBasicAuth( 63 | self.connect['player'], 64 | self.connect['password'] 65 | ) 66 | 67 | def _connect_to_fallbackdb(self): 68 | fallbackdb.connect(reuse_if_open=True) 69 | 70 | def sync(self): 71 | response = requests.get(f'{self.url}/sync', auth=self.auth) 72 | sync_data = response.json() 73 | 74 | wait_until = datetime.now() + timedelta(seconds=sync_data['tick']['remaining']) 75 | logger.info(f'Synchronizing with the server... Tick will start at {st.bold(wait_until.strftime("%H:%M:%S"))}.') 76 | time.sleep(sync_data['tick']['remaining']) 77 | 78 | def enqueue(self, flags, exploit, target): 79 | payload = json.dumps({ 80 | 'flags': flags, 81 | 'exploit': exploit, 82 | 'target': target, 83 | 'player': self.connect['player'] 84 | }) 85 | 86 | if target in self.game['team_ip']: 87 | try: 88 | response = requests.post(f'{self.url}/vuln-report', data=payload, headers=headers, auth=self.auth) 89 | except Exception: 90 | pass 91 | return {'own': len(flags)} 92 | 93 | try: 94 | response = requests.post( 95 | f'{self.url}/enqueue', data=payload, headers=headers, auth=self.auth) 96 | except Exception: 97 | for flag_value in flags: 98 | with fallbackdb.atomic(): 99 | FallbackFlag.create(value=flag_value, exploit=exploit, target=target, 100 | status='pending') 101 | return {'pending': len(flags)} 102 | else: 103 | return response.json() 104 | 105 | def enqueue_from_fallback(self, flags): 106 | payload = json.dumps([ 107 | { 108 | 'flag': flag.value, 109 | 'exploit': flag.exploit, 110 | 'target': flag.target, 111 | 'player': self.connect['player'], 112 | 'timestamp': flag.timestamp.timestamp() 113 | } for flag in flags 114 | ]) 115 | 116 | try: 117 | response = requests.post( 118 | f'{self.url}/enqueue-fallback', data=payload, headers=headers, auth=self.auth) 119 | response.raise_for_status() 120 | except Exception: 121 | logger.error("Server is unavailable. Skipping...") 122 | else: 123 | with fallbackdb.atomic(): 124 | FallbackFlag.update(status='forwarded').where(FallbackFlag.value.in_([flag.value for flag in flags])).execute() 125 | 126 | def trigger_submit(self): 127 | payload = json.dumps({ 128 | 'player': self.connect['player'] 129 | }) 130 | response = requests.post(f'{self.url}/trigger-submit', data=payload, headers=headers, auth=self.auth) 131 | return response.json() 132 | 133 | def get_flagstore_stats(self): 134 | response = requests.get(f'{self.url}/flagstore-stats', auth=self.auth) 135 | return response.json() 136 | -------------------------------------------------------------------------------- /web/src/components/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 146 | 147 | -------------------------------------------------------------------------------- /dsl.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from models import Flag 3 | from pyparsing import * 4 | 5 | # Parsing formats 6 | def parse_timedelta(tokens): 7 | tokens = tokens[0] 8 | value = int(tokens[0]) 9 | unit = tokens[1].lower() 10 | if unit in seconds: 11 | return datetime.now() - timedelta(seconds=value) 12 | if unit in minutes: 13 | return datetime.now() - timedelta(minutes=value) 14 | if unit in hours: 15 | return datetime.now() - timedelta(hours=value) 16 | 17 | def parse_time(tokens): 18 | tokens = tokens[0] 19 | hour = int(tokens[0]) 20 | minute = int(tokens[1]) 21 | second = int(tokens[2]) if len(tokens) > 2 else 0 22 | 23 | return datetime.now().replace( 24 | hour=hour, 25 | minute=minute, 26 | second=second, 27 | microsecond=0 28 | ) 29 | 30 | # Symbols 31 | and_ = ['and', '&', '&&', ','] 32 | or_ = ['or', '|', '||'] 33 | not_ = ['not', '~', '!'] 34 | eq = ['==', '=', 'equals', 'eq', 'is'] 35 | ne = ['!=', '<>', 'not equals', 'ne', 'is not'] 36 | gt = ['>', 'gt', 'over', 'above', 'greater than'] 37 | lt = ['<', 'lt', 'under', 'below', 'less than'] 38 | ge = ['>=', 'ge', 'min', 'not under', 'not below', 'after'] 39 | le = ['<=', 'le', 'max', 'not over', 'not above', 'before'] 40 | between = ['between'] 41 | matches = ['matches', 'matching', 'regex'] 42 | in_ = ['in', 'of'] 43 | not_in = ['not in', 'not of'] 44 | contains = ['contains', 'containing'] 45 | starts = ['starts with', 'starting with', 'begins with', 'beginning with'] 46 | ends = ['ends with', 'ending with'] 47 | seconds = ['s', 'sec', 'second', 'seconds'] 48 | minutes = ['m', 'min', 'mins', 'minute', 'minutes'] 49 | hours = ['h', 'hour', 'hours'] 50 | 51 | comparisons = eq + ne + gt + lt + ge + le 52 | wildcards = contains + starts + ends 53 | time_units = seconds + minutes + hours 54 | 55 | # Field 56 | field = Word(alphas, alphanums) 57 | 58 | # Values 59 | value = QuotedString(quoteChar='"', unquoteResults=True, escChar='\\') | QuotedString(quoteChar="'", unquoteResults=True, escChar='\\') | Word(printables, excludeChars='[](),') 60 | value_list = Group(Suppress('[') + DelimitedList(value, delim=',', allow_trailing_delim=True) + Suppress(']'), aslist=True) 61 | value_timedelta = Group(Word(nums) + one_of(time_units, caseless=True) + Suppress(CaselessKeyword('ago'))).set_parse_action(parse_timedelta) 62 | value_time = Group(Regex('2[0-3]|[01]?\d') + Suppress(one_of(': . - ')) + Regex('[0-5]?\d') + Optional(Suppress(one_of(': . - ')) + Regex('[0-5]?\d'))).set_parse_action(parse_time) 63 | value_instant = value_time | value_timedelta 64 | value_range = Group(Suppress('[') + value_instant + Suppress(',') + value_instant + Suppress(']')) | \ 65 | Group(value_instant + Suppress(CaselessKeyword('and')) + value_instant) | \ 66 | Group(Suppress('[') + value + Suppress(',') + value + Suppress(']')) | \ 67 | Group(value + Suppress(CaselessKeyword('and')) + value) 68 | 69 | # Conditions 70 | wildcard_condition = Group(field + one_of(wildcards, caseless=True) + value) 71 | matches_condition = Group(field + one_of(matches, caseless=True) + value) 72 | compare_condition = Group(field + one_of(comparisons, caseless=True) + (value_instant | value)) 73 | between_condition = Group(field + one_of(between, caseless=True) + value_range) 74 | in_condition = Group(field + one_of(in_ + not_in, caseless=True) + value_list) 75 | 76 | condition = wildcard_condition | matches_condition | compare_condition | between_condition | in_condition 77 | 78 | # Logical operators 79 | NOT = one_of(not_, caseless=True) 80 | AND = one_of(and_, caseless=True) 81 | OR = one_of(or_, caseless=True) 82 | 83 | def makeLRlike(numterms): 84 | if numterms is None: 85 | initlen = 2 86 | incr = 1 87 | else: 88 | initlen = {0:1,1:2,2:3,3:5}[numterms] 89 | incr = {0:1,1:1,2:2,3:4}[numterms] 90 | 91 | def pa(s,l,t): 92 | t = t[0] 93 | if len(t) > initlen: 94 | ret = t[:initlen] 95 | i = initlen 96 | while i < len(t): 97 | ret = [ret] + t[i:i+incr] 98 | i += incr 99 | return [ret] 100 | return pa 101 | 102 | boolean_condition = infixNotation( 103 | condition, 104 | [ 105 | (NOT, 1, opAssoc.RIGHT, makeLRlike(None)), 106 | (AND, 2, opAssoc.LEFT, makeLRlike(2)), 107 | (OR, 2, opAssoc.LEFT, makeLRlike(2)) 108 | ] 109 | ) 110 | 111 | def build_query(tree): 112 | if len(tree) == 2 and tree[0] in not_: 113 | return ~(build_query(tree[1])) 114 | 115 | if isinstance(tree, str): 116 | return tree 117 | 118 | if len(tree) == 3 and isinstance(tree[0], str): 119 | field, relation, value = tree 120 | rel = relation.lower() 121 | 122 | if rel in eq: 123 | return (getattr(Flag, field) == value) 124 | elif rel in ne: 125 | return (getattr(Flag, field) != value) 126 | elif rel in lt: 127 | return (getattr(Flag, field) < value) 128 | elif rel in gt: 129 | return (getattr(Flag, field) > value) 130 | elif rel in le: 131 | return (getattr(Flag, field) <= value) 132 | elif rel in ge: 133 | return (getattr(Flag, field) >= value) 134 | elif rel in matches: 135 | return (getattr(Flag, field).regexp(value)) 136 | elif rel in in_: 137 | return (getattr(Flag, field).in_(value)) 138 | elif rel in not_in: 139 | return (getattr(Flag, field).not_in(value)) 140 | elif rel in contains: 141 | return (getattr(Flag, field).contains(value)) 142 | elif rel in starts: 143 | return (getattr(Flag, field).startswith(value)) 144 | elif rel in ends: 145 | return (getattr(Flag, field).endswith(value)) 146 | elif rel in between: 147 | return (getattr(Flag, field).between(*value)) 148 | 149 | left, operator, right = tree 150 | if operator.lower() in and_: 151 | return (build_query(left) & build_query(right)) 152 | elif operator.lower() in or_: 153 | return (build_query(left) | build_query(right)) 154 | 155 | 156 | def parse_query(input): 157 | return boolean_condition.parse_string(input)[0] 158 | -------------------------------------------------------------------------------- /util/validation.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate, ValidationError 2 | from util.styler import TextStyler as st 3 | from util.log import logger 4 | 5 | 6 | def validate_data(data, schema, custom=None): 7 | try: 8 | validate(instance=data, schema=schema) 9 | 10 | if custom: 11 | custom(data) 12 | 13 | return True 14 | except ValidationError as e: 15 | path = '.'.join((str(x) for x in e.path)) 16 | logger.error(f"Error found in field {st.bold(path)}: {e.message}") 17 | return False 18 | 19 | 20 | def validate_targets(exploits): 21 | for exploit_idx, exploit in enumerate(exploits): 22 | for target_idx, target in enumerate(exploit['targets']): 23 | if not validate_ip_range(target): 24 | raise ValidationError(f"Target '{st.bold(target)}' in exploit '{st.bold(exploit['name'])}' is not a valid IP or IP range.", 25 | path=['exploits', exploit_idx, 'targets', target_idx]) 26 | 27 | 28 | def validate_quartet(quartet): 29 | return quartet.isdigit() and 0 <= int(quartet) <= 255 30 | 31 | 32 | def validate_ip(ip): 33 | quartets = ip.split(".") 34 | 35 | return len(quartets) == 4 and all(validate_quartet(quartet) for quartet in quartets) 36 | 37 | 38 | def validate_range_quartet(quartet): 39 | if "-" in quartet: 40 | start, end = quartet.split("-") 41 | return start.isdigit() and end.isdigit() and 0 <= int(start) <= int(end) <= 255 42 | else: 43 | return validate_quartet(quartet) 44 | 45 | 46 | def validate_ip_range(ip_range): 47 | if not '-' in ip_range: 48 | return validate_ip(ip_range) 49 | 50 | quartets = ip_range.split(".") 51 | 52 | return len(quartets) == 4 and all(validate_range_quartet(quartet) for quartet in quartets) 53 | 54 | 55 | def validate_delay(server_yaml_data): 56 | tick_duration = server_yaml_data['game']['tick_duration'] 57 | delay = server_yaml_data['submitter']['delay'] 58 | 59 | if delay >= tick_duration: 60 | raise ValidationError(f"Submitter delay ({delay}s) takes longer than the tick itself ({tick_duration}s).", path=['submitter', 'delay']) 61 | 62 | 63 | connect_schema = { 64 | "type": "object", 65 | "properties": { 66 | "protocol": { 67 | "type": "string", 68 | "enum": ["http", "https"] 69 | }, 70 | "host": { 71 | "type": "string", 72 | "format": "hostname", 73 | }, 74 | "port": { 75 | "type": "integer", 76 | "minimum": 1024, 77 | "maximum": 65535 78 | }, 79 | "player": { 80 | "type": "string", 81 | "maxLength": 20 82 | }, 83 | "password": { 84 | "type": "string" 85 | } 86 | }, 87 | "additionalProperties": False 88 | } 89 | 90 | 91 | exploit_schema = { 92 | "type": "object", 93 | "properties": { 94 | "name": { 95 | "type": "string", 96 | "maxLength": 100 97 | }, 98 | "timeout": { 99 | "type": "number", 100 | "exclusiveMinimum": 0 101 | }, 102 | "module": { 103 | "type": "string", 104 | }, 105 | "run": { 106 | "type": "string", 107 | }, 108 | "prepare": { 109 | "type": "string", 110 | }, 111 | "cleanup": { 112 | "type": "string", 113 | }, 114 | "env": { 115 | "type": "object", 116 | "patternProperties": { 117 | ".*": { 118 | "type": "string" 119 | } 120 | } 121 | }, 122 | "delay": { 123 | "type": "number", 124 | "exclusiveMinimum": 0 125 | }, 126 | "batches": { 127 | "type": "object", 128 | "properties": { 129 | "count": {"type": "integer", "minimum": 1}, 130 | "size": {"type": "integer", "minimum": 1}, 131 | "wait": {"type": "number", "exclusiveMinimum": 0} 132 | }, 133 | "oneOf": [ 134 | {"required": ["wait", "count"], "not": {"required": ["size"]}}, 135 | {"required": ["wait", "size"], "not": {"required": ["count"]}} 136 | ] 137 | }, 138 | "targets": { 139 | "type": "array", 140 | "items": { 141 | "type": "string", 142 | "format": "hostname" 143 | } 144 | }, 145 | }, 146 | "required": ["name", "targets"], 147 | "additionalProperties": False 148 | } 149 | 150 | 151 | exploits_schema = { 152 | "type": "array", 153 | "items": exploit_schema 154 | } 155 | 156 | 157 | game_schema = { 158 | "type": "object", 159 | "properties": { 160 | "tick_duration": { 161 | "type": "number", 162 | "exclusiveMinimum": 0 163 | }, 164 | "flag_format": { 165 | "type": "string" 166 | }, 167 | "team_ip": { 168 | "oneOf": [ 169 | {"type": "string", "format": "hostname"}, 170 | {"type": "array", "items": {"type": "string", "format": "hostname"}}, 171 | ] 172 | }, 173 | }, 174 | "required": ["tick_duration", "flag_format", "team_ip"], 175 | "additionalProperties": False, 176 | } 177 | 178 | 179 | submitter_schema = { 180 | "type": "object", 181 | "properties": { 182 | "delay": {"type": "number", "exclusiveMinimum": 0}, 183 | "module": {"type": "string"}, 184 | }, 185 | "required": ["delay"], 186 | "additionalProperties": False, 187 | } 188 | 189 | 190 | server_schema = { 191 | "type": "object", 192 | "properties": { 193 | "host": {"type": "string", "format": "hostname"}, 194 | "port": {"type": "integer", "minimum": 1024, "maximum": 65535}, 195 | "password": {"type": "string"} 196 | }, 197 | "additionalProperties": False, 198 | } 199 | 200 | 201 | database_schema = { 202 | "type": "object", 203 | "properties": { 204 | "name": {"type": "string"}, 205 | "user": {"type": "string"}, 206 | "password": {"type": "string"}, 207 | "host": {"type": "string", "format": "hostname"}, 208 | "port": {"type": "integer", "minimum": 1024, "maximum": 65535}, 209 | }, 210 | "additionalProperties": False, 211 | } 212 | 213 | 214 | server_yaml_schema = { 215 | "type": "object", 216 | "properties": { 217 | "game": game_schema, 218 | "submitter": submitter_schema, 219 | "server": server_schema, 220 | "database": database_schema 221 | }, 222 | "required": ["game", "submitter"], 223 | } 224 | -------------------------------------------------------------------------------- /docs/user-manual/server/configuration.md: -------------------------------------------------------------------------------- 1 | # Fast Server Configuration 2 | 3 | Server configuration is stored inside a YAML file named `server.yaml`. Fast looks for the configuration file within the current working directory. This allows having multiple separate configurations and environments for different competitions. 4 | 5 | The `server.yaml` file is composed of multiple sections, each used for configuring different aspects of the tool. These sections are described in detail below. 6 | 7 | ## Game Settings required { #game-settings data-toc-label="Game Settings" } 8 | 9 | The `game` section includes game-related settings that should match the competition's requirements and your team's properties within the competition. This configuration is retrieved by the clients, allowing them to extract flags based on the flag format and synchronize the attacks with the server's tick timing. This way you have to configure only the server, while the clients will automatically configure themselves upon connecting. 10 | 11 | ### Examples 12 | 13 | *a. Complete configuration* 14 | 15 | ```yaml 16 | game: 17 | tick_duration: 80 18 | flag_format: ENO[A-Za-z0-9+\/=]{48} 19 | team_ip: 10.1.26.1 20 | ``` 21 | 22 | ??? example "About the Example" 23 | 24 | Exploits will reload and rerun every 80 seconds, flags will be collected using the given regex, and alerts will appear on the dashboard each time an exploit retrieves a flag from your own service (target `10.1.26.1`). 25 | 26 | ### Options 27 | 28 | The section starts with the keyword `game:` placed anywhere at the root level of the file. 29 | 30 | `tick_duration` required 31 | 32 | : Tick duration in seconds. The duration is given by the competition organizers. 33 | 34 | `flag_format` required 35 | 36 | : Regex pattern for flag matching. The pattern is given by the competition organizers. Knowing the pattern allows the clients to extract flags from exploit scripts' return values. 37 | 38 | `team_ip` required 39 | 40 | : Your team's IP address. Fast will not submit flags originating from this IP. Instead, it will trigger an alert on the dashboard indicating that your exploit affects your own service and immediate patching is required. 41 | 42 | To specify multiple IP addresses (e.g. for Ubuntu, Fedora and Windows machines), use a list like `[10.1.26.1, 10.1.26.2, 10.1.26.3]`. 43 | 44 | ## Submitter required { #submitter data-toc-label="Submitter" } 45 | 46 | The `submitter` section is used for configuring the delay and optionally the module used for flag submission. The submitter module (default `submitter.py`) must be placed in the current working directory. For more details on writing this module, read the [Submitter Guideline](submitter-guideline.md). 47 | 48 | ### Examples 49 | 50 | *a. Minimal* 51 | 52 | ```yaml 53 | submitter: 54 | delay: 20 55 | ``` 56 | 57 | ??? example "About the Example" 58 | 59 | Flags will be submitted 20 seconds after the beginning of each tick using the `submitter.py` script placed in the same directory. 60 | 61 | *b. Setting a custom module name* 62 | 63 | ```yaml 64 | submitter: 65 | delay: 20 66 | module: ecsc_submitter_v2 67 | ``` 68 | 69 | ??? example "About the Example" 70 | 71 | Flags will be submitted 20 seconds after the beginning of each tick using the `ecsc_submitter_v2.py` script placed in the same directory. 72 | 73 | 74 | ### Options 75 | 76 | The section starts with the keyword `submitter:` placed anywhere at the root level of the file. 77 | 78 | `delay` required 79 | 80 | : Number of seconds to wait before submitting the flags. The time is relative to the beginning of the tick. 81 | 82 | !!! hint 83 | 84 | Choose a value based on the estimated time it takes for all your exploits to complete. Try not to submit too early or too late. 85 | 86 | `module` default = `submitter` 87 | 88 | : Custom name of your submitter module. Omit this field if your submitter module is named `submitter.py`; otherwise, name it to match its module name (without *.py* extension). 89 | 90 | ## Server 91 | 92 | The `server` section is used for configuring the gevent server Fast runs on. That includes configuring the host, port, and the password. 93 | 94 | These settings must be shared with everyone on the team running Fast clients, allowing them to configure the necessary [connection parameters](../client/configuration.md#connecting). 95 | 96 | Omitting this section results in using the default settings, making the server available on port `2023` with no password required. 97 | 98 | ### Examples 99 | 100 | *a. Running on a custom port and setting the HTTP Basic Auth password* 101 | 102 | ```yaml 103 | server: 104 | port: 80 105 | password: Noflags4you! 106 | ``` 107 | 108 | ??? example "About the Example" 109 | 110 | Fast server will run on the port 80 and will require a password for connecting and accessing the web dashboard. 111 | 112 | ### Options 113 | 114 | The section starts with the keyword `server:` placed anywhere at the root level of the file. 115 | 116 | `host` default = `0.0.0.0` 117 | 118 | : Host address on which the server will run. By default, it will listen on all available network interfaces. 119 | 120 | `port` default = `2023` 121 | 122 | : Port number on which the server will accept connections. Default is `2023`. 123 | 124 | `password` default = `None` 125 | 126 | : Enables HTTP Basic Authentication and sets the password for Fast clients and web dashboard. Omit this field to disable password authentication. 127 | It's highly recommended to set a password to deter unauthorized access, especially if your server is publicly accessible (e.g. running on a VPS). 128 | 129 | ## Database Connection 130 | 131 | The `database` section is used for configuring the parameters for connecting to the Postgres database used for storing flags. This includes the database **name**, **user**, **password**, **host**, and **port**. 132 | 133 | Omitting this section results in using the default values, making Fast connect to a database named `fast` on `localhost:5432` with the credentials `admin:admin`. 134 | 135 | You can execute the following command to spin up a "default" database locally using Docker: 136 | 137 | ```sh 138 | docker pull postgres:alpine && docker run --name "fast_database_container" -e POSTGRES_DB="fast" -e POSTGRES_USER="admin" -e POSTGRES_PASSWORD="admin" -p 5432:5432 -d postgres 139 | ``` 140 | 141 | You can use the same command to run Postgres Docker image with different variables. The database may be hosted on the same machine or on a separate server, depending on your preference and setup requirements. 142 | 143 | ### Examples 144 | 145 | *a. Setting database name and credentials* 146 | 147 | ```yaml 148 | database: 149 | name: fast_db_2023 150 | user: cyberhero 151 | password: zU189&63!Ixq 152 | ``` 153 | 154 | ??? example "About the Example" 155 | 156 | Fast server will connect to a database named `fast_db_2023` running on `localhost` at port `5432`, with the credentials `cyberhero:zU189&63!Ixq`. 157 | 158 | ### Options 159 | 160 | The section starts with the keyword `database:` placed anywhere at the root level of the file. 161 | 162 | `name` default = `fast` 163 | : Name of the database. 164 | 165 | `user` default = `admin` 166 | : Username for authenticating with the database. 167 | 168 | `password` default = `admin` 169 | : Password for authenticating with the database. 170 | 171 | `host` default = `localhost` 172 | : Host address of the database server. By default, Fast will connect to a database running on localhost. 173 | 174 | `port` default = `5432` 175 | : Port number on which the database server is listening. Default is `5432`, same as the Postgres default. 176 | 177 | -------------------------------------------------------------------------------- /web/src/components/Analytics.vue: -------------------------------------------------------------------------------- 1 | 221 | 222 | -------------------------------------------------------------------------------- /web/src/components/FlagBrowser.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 236 | 237 | -------------------------------------------------------------------------------- /runner.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | import time 5 | import shlex 6 | import argparse 7 | import threading 8 | import subprocess 9 | from util.helpers import truncate 10 | from importlib import import_module 11 | from handler import SubmitClient 12 | from models import Batching 13 | from util.styler import TextStyler as st 14 | from util.log import logger, log_error, log_warning 15 | 16 | exploit_name = '' 17 | handler: SubmitClient = None 18 | 19 | 20 | def main(args): 21 | global exploit_name, handler 22 | handler = SubmitClient() 23 | exploit_name = args.name 24 | 25 | if args.run: 26 | exploit_func = exploit_func_from_shell(args.run) 27 | prepare_func = None 28 | cleanup_func = None 29 | else: 30 | sys.path.append(os.getcwd()) 31 | module = import_module(f'{args.module}') 32 | exploit_func = getattr(module, 'exploit') 33 | prepare_func = getattr(module, 'prepare', None) 34 | cleanup_func = getattr(module, 'cleanup', None) 35 | 36 | if args.prepare: 37 | def prepare_func(): return run_shell_command(args.prepare) 38 | if args.cleanup: 39 | def cleanup_func(): return run_shell_command(args.cleanup) 40 | 41 | threads = [ 42 | threading.Thread( 43 | target=exploit_wrapper, 44 | name=target, 45 | args=(exploit_func, target)) 46 | for target in args.targets 47 | ] 48 | 49 | batching = Batching( 50 | args.batch_count or None, 51 | args.batch_size or None, 52 | args.batch_wait or None 53 | ) if args.batch_wait else None 54 | 55 | if prepare_func: 56 | prepare_func() 57 | 58 | if batching: 59 | batches = batch_by_count(threads, batching.count) if batching.count else batch_by_size(threads, batching.size) 60 | for idx, threads in enumerate(batches): 61 | logger.info(f"Running batch {idx + 1}/{len(batches)} of {st.bold(exploit_name)} at {st.bold(len(threads))} targets.") 62 | for t in threads: 63 | t.start() 64 | 65 | if idx < len(batches) - 1: 66 | time.sleep(batching.wait) 67 | else: 68 | for t in threads: 69 | t.start() 70 | 71 | for t in join_threads(threads, args.timeout): 72 | logger.error( 73 | f"{st.bold(exploit_name)} took longer than {st.bold(str(args.timeout))} seconds for {st.bold(t.name)}. ⌛") 74 | 75 | if cleanup_func: 76 | cleanup_func() 77 | 78 | 79 | def exploit_wrapper(exploit_func, target): 80 | try: 81 | response_text = exploit_func(target) 82 | found_flags = match_flags(response_text) 83 | 84 | if found_flags: 85 | response = handler.enqueue(found_flags, exploit_name, target) 86 | 87 | if 'own' in response: 88 | logger.warning(f"{st.bold(exploit_name)} retrieved own flag! Patch the service ASAP.") 89 | return 90 | elif 'pending' in response: 91 | logger.warning(f"{st.bold(exploit_name)} retrieved {response['pending']} flag{'s' if response['pending'] > 1 else ''}, but there is no connection to the server.") 92 | return 93 | 94 | new_flags, duplicate_flags = response['new'], response['duplicates'], 95 | new_flags_count, duplicate_flags_count = len( 96 | new_flags), len(duplicate_flags) 97 | 98 | if new_flags_count == 0 and duplicate_flags_count > 0: 99 | logger.warning(f"{st.bold(exploit_name)} retrieved no new flags and " + 100 | ("a duplicate flag " if duplicate_flags_count == 1 else f"{st.bold(duplicate_flags_count)} duplicate flags ") + 101 | f"from {st.bold(target)}.") 102 | 103 | elif new_flags_count > 0 and duplicate_flags_count > 0: 104 | logger.success(f"{st.bold(exploit_name)} retrieved " + 105 | ("a new flag " if new_flags_count == 1 else f"{st.bold(new_flags_count)} new flags, and ") + 106 | ("a duplicate flag " if duplicate_flags_count == 1 else f"{st.bold(duplicate_flags_count)} duplicate flags ") + 107 | f"from {st.bold(target)}. 🚩 — {st.faint(truncate(' '.join(new_flags), 50))}") 108 | 109 | elif new_flags_count > 0 and duplicate_flags_count == 0: 110 | logger.success(f"{st.bold(exploit_name)} retrieved " + 111 | ("a new flag " if new_flags_count == 1 else f"{st.bold(new_flags_count)} new flags ") + 112 | f"from {st.bold(target)}. 🚩 — {st.faint(truncate(' '.join(new_flags), 50))}") 113 | else: 114 | logger.warning( 115 | f"{st.bold(exploit_name)} retrieved no flags from {st.bold(target)}. — {st.color(repr(truncate(response_text, 50)), 'yellow')}") 116 | log_warning(exploit_name, target, response_text) 117 | except Exception as e: 118 | exception_name = '.'.join([type(e).__module__, type(e).__qualname__]) 119 | logger.error( 120 | f"{st.bold(exploit_name)} failed with an error for {st.bold(target)}. — {st.color(exception_name, 'red')}") 121 | 122 | log_error(exploit_name, target, e) 123 | 124 | 125 | def exploit_func_from_shell(command): 126 | def exploit_func(target): 127 | rendered_command = shlex.join([target if value == 128 | '[ip]' else value for value in shlex.split(command)]) 129 | return run_shell_command(rendered_command) 130 | 131 | return exploit_func 132 | 133 | 134 | def run_shell_command(command): 135 | return subprocess.run( 136 | command, 137 | capture_output=True, 138 | shell=True, 139 | text=True 140 | ).stdout.strip() 141 | 142 | 143 | def match_flags(text): 144 | matches = re.findall(handler.game['flag_format'], text) 145 | return matches if matches else None 146 | 147 | 148 | def join_threads(threads, timeout): 149 | start = now = time.time() 150 | while now <= (start + timeout): 151 | for thread in threads: 152 | if thread.is_alive(): 153 | thread.join(timeout=0) 154 | if all(not t.is_alive() for t in threads): 155 | return [] 156 | time.sleep(0.1) 157 | now = time.time() 158 | else: 159 | return [t for t in threads if t.is_alive()] 160 | 161 | 162 | def batch_by_size(threads, size): 163 | return [threads[i:i + size] for i in range(0, len(threads), size)] 164 | 165 | 166 | def batch_by_count(threads, count): 167 | size = len(threads) // count 168 | remainder = len(threads) % count 169 | batches = [threads[i * size : (i + 1) * size] for i in range(count)] 170 | for i in range(remainder): 171 | batches[i].append(threads[count * size + i]) 172 | return batches 173 | 174 | 175 | if __name__ == "__main__": 176 | parser = argparse.ArgumentParser( 177 | description="Run exploits in parallel for given IP addresses.") 178 | parser.add_argument("targets", metavar="IP", type=str, 179 | nargs="+", help="An IP address of the target") 180 | parser.add_argument("--name", metavar="Name", type=str, 181 | required=True, help="Name of the exploit for its identification") 182 | parser.add_argument("--module", metavar="Exploit", type=str, 183 | help="Name of the module containing the 'exploit' function") 184 | parser.add_argument("--run", metavar="Command", type=str, 185 | help="Optional shell command for running the exploit if it is not a Python script") 186 | parser.add_argument("--prepare", metavar="Command", type=str, 187 | help="Run prepare command from the module before attacking") 188 | parser.add_argument("--cleanup", metavar="Command", type=str, 189 | help="Run cleanup command from the module after attacking") 190 | parser.add_argument("--timeout", type=int, default=30, 191 | help="Optional timeout for exploit in seconds") 192 | parser.add_argument("--batch-size", type=int, default=None, 193 | help="Split targets list into batches of given size.") 194 | parser.add_argument("--batch-count", type=int, default=None, 195 | help="Split targets list into given number of batches of equal size.") 196 | parser.add_argument("--batch-wait", type=int, default=None, 197 | help="Number of seconds to wait for between running each batch.") 198 | 199 | args = parser.parse_args() 200 | main(args) 201 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This quickstart guide outlines the minimal steps to get Fast up and running. This includes setting up the database, writing the submitter module, configuring and running a Fast server, configuring a Fast client and running a single exploit. 5 | 6 | If you are in a competition and your teammate has already configured and launched the server, you can skip to [Setting up Fast Client](#setting-up-fast-client). 7 | 8 | ## Setting up Fast Server 9 | 10 | ### 1. Setup Postgres 11 | 12 | The quickest way of setting up a Postgres database is using Docker. Execute the following command on your submitter-dedicated machine to establish a database that works with the default Fast configuration: 13 | 14 | ```sh 15 | docker pull postgres:alpine && docker run --name "fastdb" -e POSTGRES_DB="fast" -e POSTGRES_USER="admin" -e POSTGRES_PASSWORD="admin" -p 5432:5432 -d postgres 16 | ``` 17 | 18 | !!! note 19 | 20 | You can find detailed instructions on configuring the database connection in the [User Manual](user-manual/server/configuration.md#database-connection). 21 | 22 | ### 2. Configure Fast Server 23 | 24 | After [installing Fast](index.md) on your submitter-dedicated machine, navigate to an empty directory and create a file named `server.yaml`. An example minimal configuration is as follows: 25 | 26 | ```yaml 27 | game: 28 | tick_duration: 80 29 | flag_format: ENO[A-Za-z0-9+\/=]{48} 30 | team_ip: 10.1.26.1 31 | 32 | submitter: 33 | delay: 20 34 | ``` 35 | 36 | Change the tick duration, flag format (regex pattern) and your vulnbox IP to match your competition requirements. Submitter delay is set to 20 seconds, meaning that flag submission will run 20 seconds after the start of each tick. Feel free to change this value. 37 | 38 | By default, the server runs on host `0.0.0.0`, port `2023`, without a password, and connects to the database set up in the previous step. 39 | 40 | !!! note 41 | 42 | Advanced configuration and every YAML section (`game`, `submitter`, `server` and `database`) is described in detail in the [User Manual](user-manual/server/configuration.md). 43 | 44 | ### 3. Write the Submitter Module 45 | 46 | In the same diretory, create a file named `submitter.py`. This will be a script that does the actual flag submission. To work properly with Fast, it should follow this simple guideline: 47 | 48 | The submitter script must define a function named `submit` that takes a list of flags (as string values) ready for submission. The `submit` function submits the flags and returns the responses from the flag-checking service as a tuple of two dictionaries: the first dictionary for the accepted flags and the other one for the rejected ones. The keys of the dictionaries are the flags, and the values are the corresponding responses from the flag-checking service. 49 | 50 | You can adapt the submit function to work with various flag submission mechanisms, such as submitting through a REST API, or over a raw TCP connection. See below for examples. 51 | 52 | === "HTTP" 53 | 54 | ```python 55 | import requests 56 | 57 | def submit(flags): 58 | flag_responses = requests.post('http://example.ctf/flags', json=flags).json() 59 | accepted_flags = { item['flag']: item['response'] for item in flag_responses if item['response'].endswith('OK') } 60 | rejected_flags = { item['flag']: item['response'] for item in flag_responses if not item['response'].endswith('OK') } 61 | return accepted_flags, rejected_flags 62 | ``` 63 | 64 | === "Raw TCP" 65 | 66 | ```python 67 | from pwn import * 68 | 69 | def submit(flags): 70 | accepted_flags, rejected_flags = {} 71 | r = remote('flags.example.ctf', 1337) 72 | for flag in flags: 73 | r.sendline(flag.encode()) 74 | response = r.recvline().decode().strip() 75 | if response.endswith('OK') 76 | accepted_flags[flag] = response 77 | else: 78 | rejected_flags[flag] = response 79 | return accepted_flags, rejected_flags 80 | ``` 81 | 82 | For a detailed guide on writing exploits, refer to the [Submitter Guideline](user-manual/server/submitter-guideline.md). 83 | 84 | ### 4. Run 85 | 86 | In the same directory, run `server` command. 87 | 88 |
89 | 96 | 97 | 98 | 99 | Fast is now ready to receive and submit the flags. By default, Fast server is available on all network interfaces (that includes your local network and your team's VPN) at the port `2023`. 100 | 101 | To access the dashboard, navigate to [http://localhost:2023](http://localhost:2023) in your web browser. Your teammates will have to use your machine's IP, which may be the one on your team's local network or the public IP if you are running on a VPS. 102 | 103 | !!! warning "If you are running on a VPS" 104 | 105 | Ensure that your instance is allowed to listen on port `2023` (or the one that you specified), and that you can connect to the competition's VPN and reach the flag checking service from the VPS. If the limited number of VPN connections is a problem, [try this](troubleshooting.md#addressing-limited-number-of-vpn-connections). 106 | 107 | Also, it's highly recommended to set a password to deter unauthorized access. To set the password, add the following to your `server.yaml` file: 108 | 109 | ```yaml 110 | server: 111 | password: 112 | ``` 113 | 114 | ## Setting up Fast Client 115 | 116 | ### 1. Connect with the Server 117 | 118 | After [installing Fast](index.md) on your own (player) machine, navigate to an empty directory and create a file named `fast.yaml`. 119 | 120 | The following starter configuration is used for connecting to a Fast server running on host `192.168.13.37` and port `2023`. Replace those with the agreed values. Also, change the player name so Fast can distinguish your exploits from those of your teammates. 121 | 122 | ```yaml 123 | connect: 124 | host: 192.168.13.37 125 | port: 2023 126 | player: yourname 127 | 128 | exploits: # no exploits yet 129 | ``` 130 | 131 | Although there are no exploits yet, the configuration above is sufficient for launching Fast client. 132 | 133 | You can test the connection by running `fast` in your terminal from the same directory. If everything is OK, the client will start, synchronize with the server, and wait for the exploits. Fast will also alert you that the `exploits` section is empty, but you can ignore that for now. 134 | 135 |
136 | 143 | 144 | ### 2. Write Exploits 145 | 146 | When managed by Fast, exploit scripts must adhere to a simple guideline to ensure compatibility. 147 | 148 | **Python Scripts:** Python exploit scripts should define a function named `exploit`, taking the target's IP address as the sole parameter. This function must return a text containing one or multiple flags. That's about it, here's a minimal example: 149 | 150 | ```py 151 | import requests 152 | 153 | def exploit(target): 154 | return requests.get(f'http://{target}:1234/flag').text 155 | ``` 156 | 157 | **Non-Python Scripts:** If you're using non-Python scripts, ensure that the target's IP address can be passed as a command-line argument. The script should only output the text containing one or multiple flags to the standard output (stdout). Here's an example using a Bash script: 158 | 159 | ```bash 160 | #!/bin/bash 161 | curl -s "http://$1:1234/flag" 162 | ``` 163 | 164 | Scripts must be placed in the same directory as `fast.yaml`. Directory may look like this: 165 | 166 | ``` 167 | exploits/ 168 | ├── alpha.py 169 | ├── bravo.py 170 | ├── charlie.sh 171 | └── fast.yaml 172 | ``` 173 | 174 | For more details on writing the exploits, refer to the [Exploit Guideline](user-manual/client/exploit-guideline.md). 175 | 176 | ### 3. Manage Exploits 177 | 178 | Once you've written the exploits and stored them in the directory, add them to the `exploits` section of `fast.yaml`. 179 | 180 | Here's the same configuration extended with two example Python scripts and a Bash script: 181 | 182 | ```yaml 183 | connect: 184 | host: 192.168.13.37 185 | port: 2023 186 | player: yourname 187 | 188 | exploits: 189 | - name: alpha 190 | targets: 191 | - 10.1.2-11.1 192 | 193 | - name: bravo 194 | targets: 195 | - 10.1.2.1 196 | - 10.1.6.1 197 | - 10.1.8-11.1 198 | 199 | - name: charlie 200 | run: ./charlie.sh [ip] 201 | targets: 202 | - 10.1.2-11.1 203 | ``` 204 | 205 | With this configuration, exploits `alpha.py`, `bravo.py` and `charlie.sh` will be run on the specified range of targets at the beginning of each tick. Every exploit will be run and every target will be attacked at the same time. 206 | 207 | Any modifications made to the `exploits` section are automatically applied at the beginning of the next tick, ensuring a seamless integration with ongoing game activities. 208 | 209 | !!! note 210 | 211 | If you are running an executable file directly (e.g., `./charlie.sh [ip]`), ensure that you have set the execute permission, and you have added an appropriate shebang line (`#!/bin/sh` or similar) or it's a binary file (e.g., `./rust_exploit [ip]`). 212 | 213 | ### 4. Run 214 | 215 | Run `fast` command from the same directory. 216 | 217 | Fast client will connect and synchronize with the server. Your exploits will be executed during each tick with the settings specified in your `fast.yaml`. 218 | 219 | ## Next Steps 220 | 221 | You have now successfully completed the Quickstart guide and set up a basic configuration for Fast. For more advanced usage and detailed instructions on how to use Fast effectively during a competition, refer to the sections of the User Manual: [Server](user-manual/server/overview.md), [Dashboard](user-manual/dashboard/overview.md) and [Client](user-manual/client/overview.md), 222 | -------------------------------------------------------------------------------- /docs/user-manual/client/exploit-management.md: -------------------------------------------------------------------------------- 1 | This page dives into the details of exploit management within Fast. Serving both as a comprehensive reference and a practical guide, this page illustrates the capabilities of Fast through examples, and explains how and when to utilize them. 2 | 3 | ## Overview 4 | 5 | ### Introduction 6 | 7 | All exploit management takes place in the `exploits` section of `fast.yaml`. 8 | 9 | ```yaml 10 | connect: 11 | host: 192.168.13.37 12 | port: 2023 13 | player: yourname 14 | 15 | exploits: 16 | - name: alpha 17 | targets: 18 | - 10.1.2-11.1 19 | 20 | - name: bravo 21 | targets: 22 | - 10.1.2-11.1 23 | ``` 24 | 25 | This file is designed to be continuously updated throughout the game. Any modifications made to the `exploits` section are automatically applied at the beginning of the next tick, ensuring a seamless integration with ongoing game activities. 26 | 27 | If you accidentally write an invalid configuration (bad formatting, accidental save, etc.), Fast will reuse the last working configuration until you fix it. The error messages related to your configuration will be logged to the console. 28 | 29 | ### How It Works 30 | 31 | The only two fields required to start running a Python exploit are `name` and `targets`. 32 | 33 | Fast *runner* will look for the module in the current working directory with the same name (module name, not filename) and import it dynamically. A "runner" is a child process responsible for running a single exploit, ensuring that each exploit gets its own separate interpreter and it bypasses the main process's Global Interpreter Lock (GIL). 34 | 35 | Since exploits are typically I/O bound tasks, runners utilize threading to run the exploit on multiple targets at once. To sum up, each exploit gets its own process/interpreter, and each target get its own thread within that process. 36 | 37 | For non-Python exploits it's a bit different since a non-Python script cannot be imported into Python. Instead of a thread, Fast runner will start a subprocess for each target, running shell commands provided in the `run` field. 38 | 39 | ### Basic Minimal Example 40 | 41 | === "Python" 42 | 43 | ```yaml 44 | - name: alpha 45 | targets: 46 | - 10.1.2-11.1 47 | ``` 48 | 49 | === "Non-Python" 50 | 51 | ```yaml 52 | - name: alpha 53 | run: bash alpha.sh [ip] 54 | targets: 55 | - 10.1.2-11.1 56 | ``` 57 | 58 | ## Usage 59 | 60 | ### Specifying Targets 61 | 62 | IP addresses can be listed individually, and IP ranges can be expressed using hyphens. 63 | 64 | ```yaml 65 | - name: alpha 66 | targets: 67 | - 10.1.3.1 68 | - 10.1.5.1 69 | - 10.1.7-10.1 70 | ``` 71 | 72 | !!! example "About the Example" 73 | At the beginning of each tick Fast will concurrently run the exploit *alpha* on the following targets: 74 | 75 | - 10.1.**3**.1, 10.1.**5**.1, 10.1.**7**.1, 10.1.**8**.1, 10.1.**9**.1, 10.1.**10**.1. 76 | 77 | ### Customizing Exploit Name 78 | 79 | You can set a custom exploit name that will be shown in the logs and on the web dashboard. Simply change the value of the `name` field, and set the `module` field to the actual name of your Python module. 80 | 81 | ```yaml 82 | - name: bravo v2 83 | module: bravo 84 | targets: 85 | - 10.1.2-11.1 86 | 87 | - name: charlie 88 | module: charlie3_Final_fixed 89 | targets: 90 | - 10.1.2-11.1 91 | ``` 92 | 93 | This field is mandatory when running a non-Python exploit. 94 | 95 | ```yaml 96 | - name: delta 97 | run: bash delta [ip] 98 | targets: 99 | - 10.1.2-11.1 100 | ``` 101 | 102 | ### Running a Non-Python Exploit 103 | 104 | To run a non-Python exploit, set the `run` field to a shell command used for running the exploit. Ensure that the target's IP address can be passed as a command-line argument to your exploit. In your provided command, use `[ip]` as a placeholder for the actual IP address. 105 | 106 | ```yaml 107 | - name: delta 108 | run: bash delta.sh [ip] 109 | targets: 110 | - 10.1.2-5.1 111 | ``` 112 | 113 | !!! example "About the example" 114 | 115 | At the beginning of each tick Fast will run the following commands in parallel: 116 | ``` 117 | bash delta.sh 10.1.2.1 118 | bash delta.sh 10.1.3.1 119 | bash delta.sh 10.1.4.1 120 | bash delta.sh 10.1.5.1 121 | ``` 122 | 123 | Exploits written in other languages are handled the same way: 124 | 125 | ```yaml 126 | node exploit.js [ip] 127 | bash exploit.sh [ip] 128 | java -jar exploit.jar [ip] 129 | dotnet exploit.dll [ip] 130 | ruby exploit.rb [ip] 131 | perl exploit.pl [ip] 132 | php exploit.php [ip] 133 | ./rust_exploit [ip] 134 | ./exploit.sh [ip] 135 | ``` 136 | 137 | If you are running an executable file directly (e.g., `./exploit.sh [ip]`), ensure that you have set the execute permission, and you have added an appropriate shebang line (`#!/bin/sh` or similar) or it's a binary file (e.g., `./rust_exploit [ip]`). 138 | 139 | 140 | ### Timeout Alerts 141 | 142 | The `timeout` field allows you to specify a duration in seconds within you expect an exploit to complete its run against a single target. If the exploit takes longer than the specified timeout, Fast will print an alert in the console. 143 | 144 | Please note that in the current version, Fast does not have the capability to terminate exploits that exceed the specified timeout. It is advisable to incorporate timeouts within your exploit code where applicable, such as using the `timeout` parameter in the `requests` library. 145 | 146 | ```yaml 147 | - name: echo 148 | timeout: 15 149 | targets: 150 | - 10.1.2-11.1 151 | ``` 152 | 153 | ### Environment Variables 154 | 155 | Environment variables can be set for each exploit using the `env` field. Environment variables are passed as key-value pairs. This is useful if you need to make your exploit configurable, or run the same exploit with different parameters for different targets. 156 | 157 | ```yaml 158 | - name: foxtrot 159 | env: 160 | WEBHOOK: https://webhook.site/748d0bd3-1764-4498-9d3a-b958b04b52a2 161 | ACCESS_TOKEN: Ku+j13dEIuXcAZQr2kYFgP/6Xvw= 162 | targets: 163 | - 10.1.2-11.1 164 | ``` 165 | 166 | ### Prepare and Cleanup 167 | 168 | The `prepare` and `cleanup` fields allow you to define shell commands to be executed before and after the exploit finishes running on all specified targets, respectively. 169 | 170 | ```yaml 171 | - name: golf 172 | prepare: > 173 | wget https://example.ctf/attack.json -o golf.json 174 | jq -i '{ availableTeams: .availableTeams, services: { golfapp: .services.golfapp } }' golf.json 175 | cleanup: rm golf.json 176 | targets: 177 | - 10.1.2-11.1 178 | ``` 179 | 180 | !!! example "About the Example" 181 | Before running the exploit, Fast will run the command provided in the `prepare` field, which will download the `attack.json` file and modify it in-place using `jq`. 182 | 183 | After the exploit is done with all the targets, Fast will run the command provided in the `cleanup` field, removing the file to keep the workspace clean. 184 | 185 | !!! tip "Hacker Tip" 186 | For Python exploits, a more powerful and flexible approach is to use `prepare()` and `cleanup()` Python functions within your exploit code, as they have full access to your exploit script. More details can be found in the [Exploit Guideline](exploit-guideline.md#prepare-and-cleanup). 187 | 188 | 189 | ### Optimizing with Delays 190 | 191 | In situations where running multiple exploits simultaneously can lead to huge spikes in CPU, memory and network usage, Fast offers the `delay` attribute for setting a delay in seconds before the exploit's execution begins, relative to the start of the tick. This allows you to strategically arrange your exploits, distribute the load over time, and reduce the risk of overloading your system resources. 192 | 193 | ```yaml 194 | - name: hotel 195 | targets: 196 | - 10.1.2-31.1 197 | 198 | - name: india 199 | delay: 5 200 | targets: 201 | - 10.1.2-31.1 202 | 203 | - name: juliett 204 | delay: 10 205 | targets: 206 | - 10.1.2-31.1 207 | ``` 208 | 209 | !!! example "About the Example" 210 | At the beginning of each tick Fast will run the exploit *hotel* at **30** targets. 5 seconds later it will run *india*, and after 5 more seconds it will run *juliett*. Assuming that none of these exploits take more than 5 seconds (exploits typically take much less), the peak number of concurrent attacks will be **no more than 30**, rather than up to 90. 211 | 212 | ![](../../assets/images/attacks1.png) 213 | 214 | ### Optimizing with Batching 215 | 216 | Batching is another way of distributing the load over time with the goal of mitigating CPU, memory and network usage spikes. Setting up batching allows you to divide the list of targets into smaller, equally-sized and more manageable batches. 217 | 218 | To enable batching, you need to set either the number of batches (`count`), or the size of each batch (`size`): 219 | 220 | - `count`: Specifies the total number of equal-sized batches the targets will be divided into. 221 | - `size`: Specifies the size of each batch. Number of batches will be calculated. 222 | 223 | The `wait` attribute defines the time gap in seconds between processing two consecutive batches. 224 | 225 | === "Batch by Count" 226 | 227 | ```yaml 228 | - name: kilo 229 | batches: 230 | count: 5 # Will form batches of sizes [6, 6, 6, 6, 6] 231 | wait: 2 232 | targets: 233 | - 10.1.2-31.1 234 | ``` 235 | === "Batch by Size" 236 | 237 | ```yaml 238 | - name: kilo 239 | batches: 240 | size: 8 # Will form batches of sizes [8, 8, 8, 6] 241 | wait: 2 242 | targets: 243 | - 10.1.2-31.1 244 | ``` 245 | 246 | !!! example "About the Example" 247 | At the beginning of each tick Fast will run the exploit on the first batch of targets, then wait for 2 seconds before running the next batch, repeating until it's done with all the targets. 248 | 249 | ![](../../assets/images/attacks2.png) 250 | 251 | 252 | !!! tip "Hacker Tip" 253 | 254 | If you are using batching on multiple exploits, set different delays for each exploit to prevent the batches from overlapping. This will eliminate the "stacked spikes" you may create from batching. 255 | 256 | ```yaml 257 | - name: lima 258 | batches: 259 | count: 5 260 | wait: 3 261 | targets: 262 | - 10.1.2-31.1 263 | 264 | - name: mike 265 | delay: 2 266 | batches: 267 | size: 8 268 | wait: 3 269 | targets: 270 | - 10.1.2-31.1 271 | ``` 272 | 273 | ![](../../assets/images/attacks3.png) -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import os 2 | import yaml 3 | import time 4 | import hashlib 5 | import threading 6 | import subprocess 7 | from copy import deepcopy 8 | from itertools import product 9 | from database import fallbackdb 10 | from models import ExploitDetails, FallbackFlag, Batching 11 | from handler import SubmitClient 12 | from util.styler import TextStyler as st 13 | from util.helpers import seconds_from_now 14 | from util.log import logger, create_log_dir 15 | from util.validation import validate_data, validate_targets, connect_schema, exploits_schema 16 | from requests.exceptions import HTTPError, ConnectionError, Timeout, RequestException 17 | from apscheduler.schedulers.background import BlockingScheduler 18 | 19 | DIR_PATH = os.path.dirname(os.path.realpath(__file__)) 20 | RUNNER_PATH = os.path.join(DIR_PATH, 'runner.py') 21 | 22 | 23 | cached_exploits = (None, None) # (hash, exploits) 24 | handler: SubmitClient = None 25 | connect = { 26 | 'protocol': 'http', 27 | 'host': '127.0.0.1', 28 | 'port': '2023', 29 | 'player': 'anon' 30 | } 31 | 32 | 33 | def main(): 34 | banner() 35 | create_log_dir() 36 | load_config() 37 | setup_handler() 38 | 39 | scheduler = BlockingScheduler() 40 | scheduler.add_job( 41 | func=run_exploits, 42 | trigger='interval', 43 | seconds=handler.game['tick_duration'], 44 | id='exploits', 45 | next_run_time=seconds_from_now(0) 46 | ) 47 | 48 | scheduler.add_job( 49 | func=enqueue_from_fallback, 50 | trigger='interval', 51 | seconds=handler.game['tick_duration'], 52 | id='fallback_flagstore', 53 | next_run_time=seconds_from_now(0) 54 | ) 55 | 56 | scheduler.start() 57 | 58 | 59 | def run_exploits(): 60 | exploits = load_exploits() 61 | if not exploits: 62 | logger.info(f'No exploits defined in {st.bold("fast.yaml")}. Skipped.') 63 | return 64 | 65 | for exploit in exploits: 66 | threading.Thread(target=run_exploit, args=(exploit,)).start() 67 | 68 | 69 | def run_exploit(exploit): 70 | runner_command = ['python', RUNNER_PATH] + \ 71 | exploit.targets + ['--name', exploit.name] 72 | if exploit.module: 73 | runner_command.extend(['--module', exploit.module]) 74 | if exploit.run: 75 | runner_command.extend(['--run', exploit.run]) 76 | if exploit.prepare: 77 | runner_command.extend(['--prepare', exploit.prepare]) 78 | if exploit.cleanup: 79 | runner_command.extend(['--cleanup', exploit.cleanup]) 80 | if exploit.timeout: 81 | runner_command.extend(['--timeout', str(exploit.timeout)]) 82 | if exploit.batching: 83 | runner_command.extend(['--batch-wait', str(exploit.batching.wait)]) 84 | 85 | if exploit.batching.count: 86 | runner_command.extend(['--batch-count', str(exploit.batching.count)]) 87 | elif exploit.batching.size: 88 | runner_command.extend(['--batch-size', str(exploit.batching.size)]) 89 | if exploit.delay: 90 | time.sleep(exploit.delay) 91 | 92 | logger.info( 93 | f'Running {st.bold(exploit.name)} at {st.bold(len(exploit.targets))} target{"s" if len(exploit.targets) > 1 else ""}...') 94 | 95 | subprocess.run(runner_command, text=True, env={ 96 | **exploit.env, **os.environ}) 97 | 98 | logger.info(f'{st.bold(exploit.name)} finished.') 99 | 100 | 101 | def enqueue_from_fallback(): 102 | flags = [flag for flag in FallbackFlag.select().where(FallbackFlag.status == 'pending')] 103 | if flags: 104 | logger.info(f'Forwarding {len(flags)} flags from the fallback flagstore...') 105 | handler.enqueue_from_fallback(flags) 106 | 107 | 108 | def load_exploits(): 109 | global cached_exploits 110 | 111 | with open('fast.yaml', 'r') as file: 112 | digest = hashlib.sha256(file.read().encode()).hexdigest() 113 | if cached_exploits[0] == digest: 114 | return cached_exploits[1] 115 | 116 | try: 117 | file.seek(0) 118 | logger.info('Reloading exploits...') 119 | yaml_data = yaml.safe_load(file) 120 | 121 | exploits_data = yaml_data.get('exploits', 'MISSING') 122 | if exploits_data == 'MISSING': 123 | logger.warning(f"{st.bold('exploits')} section is missing in {st.bold('fast.yaml')}. Please add {st.bold('exploits')} section to start running exploits in the next tick.") 124 | return 125 | elif exploits_data == None: 126 | logger.warning(f"{st.bold('exploits')} section contains no exploits. Please add your exploits to start running them in the next tick.") 127 | return 128 | 129 | if not validate_data(exploits_data, exploits_schema, custom=validate_targets): 130 | if cached_exploits[1]: 131 | logger.error( 132 | f"Errors found in 'exploits' section in {st.bold('fast.yaml')}. The previous configuration will be reused in the following tick.") 133 | else: 134 | logger.error( 135 | f"Errors found in 'exploits' section in {st.bold('fast.yaml')}. Please fix the errors to start running exploits in the next tick.") 136 | 137 | return cached_exploits[1] 138 | 139 | exploits = [parse_exploit_entry(exploit) 140 | for exploit in yaml_data['exploits']] 141 | logger.success(f'Loaded {st.bold(len(exploits))} exploits.') 142 | cached_exploits = (digest, exploits) 143 | return exploits 144 | except Exception as e: 145 | if cached_exploits[1]: 146 | logger.error( 147 | f"Failed to load exploits from the new {st.bold('fast.yaml')} file. The previous configuration will be reused in the following tick.") 148 | else: 149 | logger.error( 150 | f"Failed to load exploits from the new {st.bold('fast.yaml')} file. Please fix the errors to start running exploits in the next tick.") 151 | 152 | return cached_exploits[1] 153 | 154 | 155 | def expand_ip_range(ip_range): 156 | octets = ip_range.split('.') 157 | ranges = [list(range(int(octet.split('-')[0]), int(octet.split('-')[1]) + 1)) 158 | if '-' in octet else [int(octet)] for octet in octets] 159 | return ['.'.join(map(str, octets)) for octets in product(*ranges)] 160 | 161 | 162 | def parse_exploit_entry(entry): 163 | name = entry.get('name') 164 | run = entry.get('run') 165 | prepare = entry.get('prepare') 166 | cleanup = entry.get('cleanup') 167 | module = None if run else (entry.get('module') or name).replace('.py', '') 168 | targets = [ip for ip_range in entry['targets'] for ip in expand_ip_range(ip_range)] 169 | timeout = entry.get('timeout') 170 | env = entry.get('env') or {} 171 | delay = entry.get('delay') 172 | batching = Batching( 173 | entry['batches'].get('count'), 174 | entry['batches'].get('size'), 175 | entry['batches'].get('wait') 176 | ) if entry.get('batches') else None 177 | 178 | return ExploitDetails(name, targets, module, run, prepare, cleanup, timeout, env, delay, batching) 179 | 180 | 181 | def load_config(): 182 | # Load fast.yaml 183 | if not os.path.isfile('fast.yaml'): 184 | logger.error(f"{st.bold('fast.yaml')} not found in the current working directory. Exiting...") 185 | exit(1) 186 | 187 | with open('fast.yaml', 'r') as file: 188 | yaml_data = yaml.safe_load(file) 189 | 190 | if not yaml_data: 191 | logger.error(f"{st.bold('fast.yaml')} is empty. Exiting...") 192 | exit(1) 193 | 194 | # Load and validate connection config 195 | logger.info('Loading connection config...') 196 | connect_data = yaml_data.get('connect') 197 | if connect_data: 198 | connect.update(connect_data) 199 | 200 | if not validate_data(connect, connect_schema): 201 | logger.error(f"Fix errors in {st.bold('connect')} section in {st.bold('fast.yaml')} and rerun.") 202 | exit(1) 203 | 204 | # Load and validate exploits config 205 | logger.info('Checking exploits config...') 206 | exploits_data = yaml_data.get('exploits', 'MISSING') 207 | if exploits_data == 'MISSING': 208 | logger.warning(f"{st.bold('exploits')} section is missing in {st.bold('fast.yaml')}. Please add {st.bold('exploits')} section to start running exploits in the next tick.") 209 | elif exploits_data == None: 210 | logger.warning(f"{st.bold('exploits')} section contains no exploits. Please add your exploits to start running them in the next tick.") 211 | elif exploits_data and not validate_data(exploits_data, exploits_schema, custom=validate_targets): 212 | logger.error(f"Fix errors in {st.bold('exploits')} section in {st.bold('fast.yaml')} and rerun.") 213 | exit(1) 214 | 215 | logger.success('No errors found in exploits config.') 216 | 217 | 218 | def setup_handler(fire_mode=False): 219 | global handler 220 | 221 | # Fetch, apply and persist server's game configuration 222 | logger.info('Fetching game config...') 223 | 224 | if connect.get('password'): 225 | conn_str = f"{connect['protocol']}://{connect['player']}:***@{connect['host']}:{connect['port']}" 226 | else: 227 | conn_str = f"{connect['protocol']}://{connect['host']}:{connect['port']}" 228 | 229 | logger.info(f"Connecting to {st.color(conn_str, 'cyan')}") 230 | 231 | try: 232 | handler = SubmitClient(connect) 233 | 234 | config_repr = f"{handler.game['tick_duration']}s tick, {handler.game['flag_format']}, {' '.join(handler.game['team_ip'])}" 235 | logger.success(f'Game configured successfully. — {st.faint(config_repr)}') 236 | 237 | # Synchronize client with server's tick clock 238 | if not fire_mode: 239 | handler.sync() 240 | except HTTPError as e: 241 | if e.response.status_code == 401: 242 | error = f'Failed to authenticate with the Fast server. Check the password with your teammates and try again.' 243 | else: 244 | error = f'HTTP error occurred while connecting to the Fast server. Status code: {e.response.status_code}, Reason: {e.response.text}' 245 | except ConnectionError as e: 246 | error = f'Error connecting to the Fast server at URL {conn_str}. Ensure the server is up and running, your configuration is correct, and your network connection is stable.' 247 | except Timeout as e: 248 | error = f"Connection to Fast server has timed out." 249 | except RequestException as e: 250 | exception_name = '.'.join([type(e).__module__, type(e).__qualname__]) 251 | error = f"Some unexpected error occurred during connecting to Fast server. — {st.color(exception_name, 'red')}" 252 | else: 253 | error = None 254 | finally: 255 | if error: 256 | logger.error(error) 257 | exit(1) 258 | 259 | # Setup fallback db 260 | fallbackdb.connect(reuse_if_open=True) 261 | fallbackdb.create_tables([FallbackFlag]) 262 | 263 | 264 | def banner(): 265 | vers = '1.0.0' 266 | print(f""" 267 | \033[34;1m .___ ____\033[0m ______ __ 268 | \033[34;1m / /\__/ /\033[0m / ____/_ ____ / /_ 269 | \033[34;1m / / / ❬` \033[0m / /_/ __ `/ ___/ __/ 270 | \033[34;1m /___/ /____\ \033[0m / __/ /_/ (__ ) /_ 271 | \033[34;1m / \___\/ \033[0m/_/ \__,_/____/\__/ 272 | \033[34;1m/\033[0m \033[34mclient\033[0m \033[2mv{vers}\033[0m 273 | """) 274 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey 2 | monkey.patch_all() 3 | import os 4 | import sys 5 | import json 6 | import yaml 7 | import time 8 | import logging 9 | import functools 10 | from itertools import chain 11 | from base64 import b64decode 12 | from importlib import import_module, reload 13 | from datetime import datetime, timedelta 14 | from flask import Flask, request, send_from_directory 15 | from flask_httpauth import HTTPBasicAuth 16 | from flask_socketio import SocketIO, emit 17 | from flask_cors import CORS 18 | from apscheduler.schedulers.background import BackgroundScheduler 19 | from models import Flag 20 | from dsl import parse_query, build_query 21 | from peewee import fn, IntegrityError, PostgresqlDatabase 22 | from database import db 23 | from util.log import logger 24 | from util.styler import TextStyler as st 25 | from util.helpers import truncate, deep_update, flag_model_to_dict 26 | from util.validation import validate_data, validate_delay, server_yaml_schema 27 | from pyparsing.exceptions import ParseException 28 | 29 | app = Flask(__name__, static_url_path='') 30 | auth = HTTPBasicAuth() 31 | socketio = SocketIO(app, cors_allowed_origins="*") 32 | 33 | tick_number = -1 34 | tick_start = datetime.max 35 | server_start = datetime.max 36 | 37 | config = { 38 | 'game': {}, 39 | 'submitter': { 40 | 'module': 'submitter' 41 | }, 42 | 'server': { 43 | 'host': '0.0.0.0', 44 | 'port': 2023 45 | }, 46 | 'database': { 47 | 'name': 'fast', 48 | 'user': 'admin', 49 | 'password': 'admin', 50 | 'host': 'localhost', 51 | 'port': 5432 52 | } 53 | } 54 | 55 | RECOVERY_CONFIG_PATH = '.recover.json' 56 | 57 | 58 | def main(): 59 | banner() 60 | configure_flask() 61 | load_config() 62 | setup_database() 63 | recover() 64 | 65 | game, submitter, server = config['game'], config['submitter'], config['server'] 66 | 67 | # Schedule tick clock 68 | 69 | scheduler = BackgroundScheduler() 70 | scheduler.add_job( 71 | func=tick_clock, 72 | trigger='interval', 73 | seconds=game['tick_duration'], 74 | id='clock', 75 | next_run_time=tick_start 76 | ) 77 | 78 | # Schedule flag submitting 79 | 80 | delay = timedelta(seconds=submitter['delay']) 81 | interval = game['tick_duration'] 82 | first_run = tick_start + delay 83 | 84 | # Enable submitter importing 85 | sys.path.append(os.getcwd()) 86 | 87 | scheduler.add_job( 88 | func=submitter_wrapper, 89 | trigger='interval', 90 | seconds=interval, 91 | id='submitter', 92 | next_run_time=first_run 93 | ) 94 | 95 | # Run scheduler and Gevent server 96 | 97 | scheduler.start() 98 | 99 | socketio.run( 100 | app, 101 | host=server['host'], 102 | port=server['port'] 103 | ) 104 | 105 | 106 | @auth.verify_password 107 | def authenticate(username, password): 108 | return password == config['server']['password'] 109 | 110 | 111 | @auth.error_handler 112 | def unauthorized(): 113 | return {"error": "Unauthorized access"}, 401 114 | 115 | 116 | def basic(func): 117 | @functools.wraps(func) 118 | def wrapper(*args, **kwargs): 119 | if 'password' in config['server']: 120 | return auth.login_required(func)(*args, **kwargs) 121 | else: 122 | return func(*args, **kwargs) 123 | return wrapper 124 | 125 | 126 | @socketio.on('connect') 127 | def authenticate_websocket(): 128 | if 'password' not in config['server']: 129 | return True 130 | 131 | auth_header = request.headers.get('Authorization') 132 | if not auth_header: 133 | return False 134 | 135 | basic, credentials = auth_header.split(' ') 136 | if basic.lower() != 'basic': 137 | return False 138 | 139 | username, password = b64decode(credentials).decode('utf-8').split(':') 140 | 141 | return authenticate(username, password) 142 | 143 | 144 | @app.route('/enqueue', methods=['POST']) 145 | @basic 146 | def enqueue(): 147 | flags = request.json['flags'] 148 | exploit = request.json['exploit'] 149 | target = request.json['target'] 150 | player = request.json['player'] 151 | 152 | new_flags = [] 153 | duplicate_flags = [] 154 | 155 | for flag_value in flags: 156 | try: 157 | with db.atomic(): 158 | Flag.create(value=flag_value, exploit=exploit, target=target, 159 | tick=tick_number, player=player, status='queued') 160 | new_flags.append(flag_value) 161 | except IntegrityError: 162 | duplicate_flags.append(flag_value) 163 | 164 | if new_flags: 165 | logger.success(f"{st.bold(player)} retrieved " + 166 | (f"{st.bold(1)} flag " if len(new_flags) == 1 else f"{st.bold(len(new_flags))} flags ") + 167 | f"from {st.bold(target)} using {st.bold(exploit)}. 🚩 — {st.faint(truncate(' '.join(new_flags), 50))}") 168 | 169 | socketio.emit('enqueue', { 170 | 'new': len(new_flags), 171 | 'dup': len(duplicate_flags), 172 | 'player': player, 173 | 'target': target, 174 | 'exploit': exploit 175 | }) 176 | 177 | return { 178 | 'duplicates': duplicate_flags, 179 | 'new': new_flags 180 | } 181 | 182 | 183 | @app.route('/enqueue-fallback', methods=['POST']) 184 | @basic 185 | def enqueue_fallback(): 186 | flags = request.get_json() 187 | 188 | new_flags = [] 189 | duplicate_flags = [] 190 | 191 | for flag in flags: 192 | flag_value = flag['flag'] 193 | exploit = flag['exploit'] 194 | target = flag['target'] 195 | player = flag['player'] 196 | timestamp = flag.get('timestamp', None) 197 | tick = int((datetime.fromtimestamp(timestamp) - server_start).total_seconds() // config['game']['tick_duration']) if timestamp else tick_number 198 | 199 | try: 200 | with db.atomic(): 201 | Flag.create(value=flag_value, exploit=exploit, target=target, 202 | tick=tick, player=player, status='queued') 203 | new_flags.append(flag_value) 204 | except IntegrityError: 205 | duplicate_flags.append(flag_value) 206 | 207 | if new_flags: 208 | logger.success(f"{st.bold(player)} sent " + 209 | (f"{st.bold(1)} flag " if len(new_flags) == 1 else f"{st.bold(len(new_flags))} flags from fallback flagstore. 🚩")) 210 | 211 | socketio.emit('enqueue_fallback', { 212 | 'new': len(new_flags), 213 | 'dup': len(duplicate_flags) 214 | }) 215 | 216 | return { 217 | 'duplicates': duplicate_flags, 218 | 'new': new_flags 219 | } 220 | 221 | 222 | @app.route('/enqueue-manual', methods=['POST']) 223 | @basic 224 | def enqueue_manual(): 225 | flags = request.json['flags'] 226 | player = request.json.get('player') or 'anon' 227 | action = request.json.get('action') or 'submit' 228 | 229 | if action == 'enqueue': 230 | new_flags = [] 231 | duplicate_flags = [] 232 | 233 | for flag_value in flags: 234 | try: 235 | with db.atomic(): 236 | new_flag = Flag.create(value=flag_value, tick=tick_number, player=player, 237 | exploit='manual', target='unknown', status='queued') 238 | new_flags.append(new_flag) 239 | except IntegrityError: 240 | duplicate_flags.append(flag_value) 241 | 242 | return [ 243 | { 244 | 'status': flag.status, 245 | 'value': flag.value, 246 | 'persisted': flag._pk is not None 247 | } for flag in new_flags 248 | ] + [ 249 | { 250 | 'status': 'duplicate', 251 | 'value': value, 252 | 'persisted': False 253 | } for value in duplicate_flags 254 | ] 255 | elif action == 'submit': 256 | accepted, rejected = submit_func(flags) 257 | accepted_flags = [] 258 | rejected_flags = [] 259 | 260 | for value, response in accepted.items(): 261 | try: 262 | with db.atomic(): 263 | flag = Flag.create(value=value, tick=tick_number, player=player, 264 | exploit='manual', target='unknown', status='accepted', response=response) 265 | accepted_flags.append(flag) 266 | except IntegrityError: 267 | accepted_flags.append(Flag(value=value, status='accepted', response=response)) 268 | 269 | for value, response in rejected.items(): 270 | try: 271 | with db.atomic(): 272 | flag = Flag.create(value=value, tick=tick_number, player=player, 273 | exploit='manual', target='unknown', status='rejected', response=response) 274 | rejected_flags.append(flag) 275 | except IntegrityError: 276 | rejected_flags.append(Flag(value=value, status='accepted', response=response)) 277 | 278 | return [ 279 | { 280 | 'status': flag.status, 281 | 'value': flag.value, 282 | 'response': flag.response, 283 | 'persisted': flag._pk is not None 284 | } for flag in chain(accepted_flags, rejected_flags) 285 | ] 286 | else: 287 | return { 288 | 'message': 'Unknown action.' 289 | }, 400 290 | 291 | 292 | @app.route('/vuln-report', methods=['POST']) 293 | @basic 294 | def vulnerability_report(): 295 | exploit = request.json['exploit'] 296 | target = request.json['target'] 297 | player = request.json['player'] 298 | 299 | socketio.emit('vulnerabilityReported', { 300 | 'player': player, 301 | 'target': target, 302 | 'exploit': exploit 303 | }) 304 | 305 | logger.warning(f"{st.bold(player)} retrieved " + 306 | f"{st.bold('own')} flag from {st.bold(target)} using {st.bold(exploit)}! Patch the service ASAP.") 307 | 308 | return { 309 | 'message': 'Vulnerability reported.' 310 | } 311 | 312 | 313 | @app.route('/sync') 314 | @basic 315 | def sync(): 316 | now: datetime = datetime.now() 317 | 318 | duration = config['game']['tick_duration'] 319 | submit_delay = config['submitter']['delay'] 320 | 321 | elapsed = (now - tick_start).total_seconds() 322 | remaining = duration - elapsed 323 | 324 | next_submit: datetime = tick_start + timedelta(seconds=submit_delay + (duration if elapsed > submit_delay else 0)) 325 | next_submit_remaining = (next_submit - now).total_seconds() 326 | 327 | return { 328 | 'submitter': { 329 | 'remaining': next_submit_remaining, 330 | 'delay': submit_delay 331 | }, 332 | 'tick': { 333 | 'current': tick_number, 334 | 'duration': duration, 335 | 'elapsed': elapsed, 336 | 'remaining': remaining, 337 | } 338 | } 339 | 340 | 341 | @app.route('/flagstore-stats') 342 | @basic 343 | def get_flagstore_stats(): 344 | queued_count = Flag.select().where(Flag.status == 'queued').count() 345 | accepted_count = Flag.select().where(Flag.status == 'accepted').count() 346 | rejected_count = Flag.select().where(Flag.status == 'rejected').count() 347 | 348 | accepted_delta = Flag.select().where(Flag.status == 'accepted', Flag.tick == tick_number).count() 349 | rejected_delta = Flag.select().where(Flag.status == 'rejected', Flag.tick == tick_number).count() 350 | 351 | return { 352 | 'queued': queued_count, 353 | 'accepted': accepted_count, 354 | 'rejected': rejected_count, 355 | 'delta': { 356 | 'accepted': accepted_delta, 357 | 'rejected': rejected_delta 358 | } 359 | } 360 | 361 | 362 | @app.route('/exploit-analytics') 363 | @basic 364 | def get_exploit_analytics(): 365 | return generate_exploit_analytics() 366 | 367 | 368 | @app.route('/search', methods=['POST']) 369 | @basic 370 | def search(): 371 | request_json = request.json 372 | 373 | # Build search query 374 | try: 375 | parsed_query = parse_query(request_json['query']) 376 | peewee_query = build_query(parsed_query) 377 | except KeyError: 378 | return { 379 | 'error': 'Missing query.' 380 | }, 400 381 | except ParseException: 382 | return { 383 | 'error': 'Invalid query.' 384 | }, 400 385 | except AttributeError as e: 386 | return { 387 | 'error': f'Unknown field {e.args[0].split()[-1]}.' 388 | }, 400 389 | except Exception as e: 390 | return { 391 | 'error': f'Something is broken either with your query or with the way it is processed. :(' 392 | }, 500 393 | 394 | # Select page 395 | page = request_json.get('page', 1) 396 | show = min(request_json.get('show', 10), 100) 397 | 398 | # Select sorting 399 | sort_fields = request_json.get("sort", []) 400 | sort_expressions = [ 401 | getattr(Flag, item["field"]).desc() if item["direction"] == "desc" else getattr(Flag, item["field"]) 402 | for item in sort_fields 403 | ] 404 | 405 | # Run query 406 | start = time.time() 407 | try: 408 | results = [flag_model_to_dict(flag) for flag in 409 | Flag.select() 410 | .where(peewee_query) 411 | .order_by(*sort_expressions) 412 | .paginate(page, show) 413 | ] 414 | except Exception: 415 | return { 416 | 'error': f'Failed to run the query.' 417 | }, 500 418 | elapsed = time.time() - start 419 | 420 | total = Flag.select().where(peewee_query).count() 421 | total_pages = -(-total // show) 422 | 423 | metadata = { 424 | "paging": { 425 | "current": page, 426 | "last": total_pages, 427 | "hasNext": page + 1 <= total_pages, 428 | "hasPrev": page > 1 429 | }, 430 | "results": { 431 | "total": total, 432 | "fetched": len(results), 433 | "executionTime": elapsed 434 | } 435 | } 436 | 437 | return { 438 | 'results': results, 439 | 'metadata': metadata 440 | } 441 | 442 | 443 | @app.route('/config') 444 | @basic 445 | def get_config(): 446 | player = request.args['player'] 447 | address = request.remote_addr 448 | 449 | socketio.emit('playerConnect', { 450 | 'message': f'{player} has connected from {address}.' 451 | }) 452 | 453 | return config 454 | 455 | 456 | @app.route('/flag-format') 457 | @basic 458 | def get_flag_format(): 459 | return { 460 | "format": config['game']['flag_format'] 461 | } 462 | 463 | 464 | @app.route('/trigger-submit', methods=['POST']) 465 | @basic 466 | def trigger_submit(): 467 | logger.info(f"Submitter triggered manually by {st.bold(request.json['player'])}.") 468 | submitter_wrapper() 469 | 470 | return { 471 | 'message': 'Flag submission completed. Check the web dashboard.' 472 | } 473 | 474 | 475 | @app.route('/') 476 | @basic 477 | def dashboard(): 478 | return send_from_directory(app.static_folder, 'index.html') 479 | 480 | 481 | def submitter_wrapper(): 482 | flags = [flag.value for flag in 483 | Flag.select().where(Flag.status == 'queued')] 484 | 485 | if not flags: 486 | socketio.emit('submitSkip', { 487 | 'message': 'No flags in the queue! Submission skipped.' 488 | }) 489 | 490 | logger.info(f"No flags in the queue! Submission skipped.") 491 | 492 | return 493 | 494 | socketio.emit('submitStart', { 495 | 'message': f'Submitting {len(flags)} flags...' 496 | }) 497 | 498 | logger.info(st.bold(f"Submitting {len(flags)} flags...")) 499 | 500 | accepted, rejected = submit_func(flags) 501 | 502 | if accepted: 503 | logger.success(f"{st.bold(len(accepted))} flags accepted. ✅") 504 | else: 505 | logger.warning( 506 | f"No flags accepted, or your script is not returning accepted flags.") 507 | 508 | if rejected: 509 | logger.warning(f"{st.bold(len(rejected))} flags rejected.") 510 | 511 | if len(flags) != len(accepted) + len(rejected): 512 | logger.error( 513 | f"{st.bold(len(flags) - len(accepted) - len(rejected))} responses missing. Flags may be submitted, but your stats may be inaccurate.") 514 | 515 | with db.atomic(): 516 | if accepted: 517 | to_accept = Flag.select().where(Flag.value.in_(list(accepted.keys()))) 518 | for flag in to_accept: 519 | flag.status = 'accepted' 520 | flag.response = accepted[flag.value] 521 | Flag.bulk_update(to_accept, fields=[Flag.status, Flag.response]) 522 | 523 | if rejected: 524 | to_reject = Flag.select().where(Flag.value.in_(list(rejected.keys()))) 525 | for flag in to_reject: 526 | flag.status = 'rejected' 527 | flag.response = rejected[flag.value] 528 | Flag.bulk_update(to_reject, fields=[Flag.status, Flag.response]) 529 | 530 | queued_count = Flag.select().where(Flag.status == 'queued').count() 531 | accepted_count = Flag.select().where(Flag.status == 'accepted').count() 532 | rejected_count = Flag.select().where(Flag.status == 'rejected').count() 533 | 534 | socketio.emit('submitComplete', { 535 | 'message': f'{len(accepted)} flag{"s" if len(accepted) > 1 else ""} accepted, {len(rejected)} rejected.', 536 | 'data': { 537 | 'queued': queued_count, 538 | 'accepted': accepted_count, 539 | 'rejected': rejected_count, 540 | 'delta': { 541 | 'accepted': len(accepted), 542 | 'rejected': len(rejected) 543 | } 544 | } 545 | }) 546 | 547 | socketio.emit('analyticsUpdate', generate_exploit_analytics()) 548 | 549 | queued_count_st = st.color( 550 | st.bold(queued_count), 'green') if queued_count == 0 else st.bold(queued_count) 551 | 552 | accepted_count_st = st.color(st.bold( 553 | accepted_count), 'green') if accepted_count > 0 else st.color(st.bold(accepted_count), 'yellow') 554 | 555 | rejected_count_st = st.color(st.bold( 556 | rejected_count), 'green') if rejected_count == 0 else st.color(st.bold(rejected_count), 'yellow') 557 | 558 | logger.info( 559 | f"{st.bold('Stats')} — {queued_count_st} queued, {accepted_count_st} accepted, {rejected_count_st} rejected.") 560 | 561 | 562 | def tick_clock(): 563 | global tick_number, tick_start 564 | 565 | tick_number += 1 566 | tick_start = datetime.now() 567 | next_tick_start = tick_start + \ 568 | timedelta(seconds=config['game']['tick_duration']) 569 | 570 | socketio.emit('tickStart', { 571 | 'current': tick_number, 572 | }) 573 | 574 | logger.info(f'Started tick {st.bold(str(tick_number))}. ' + 575 | f'Next tick scheduled for {st.bold(next_tick_start.strftime("%H:%M:%S"))}. ⏱️') 576 | 577 | 578 | def generate_exploit_analytics(): 579 | tick_window = 10 580 | latest_tick = tick_number 581 | oldest_tick = max(0, latest_tick - tick_window + 1) # add 1 to ignore -11th tick 582 | 583 | query = Flag.select(Flag.player, Flag.exploit, Flag.tick, fn.COUNT(Flag.id).alias('flag_count')) \ 584 | .where((Flag.tick >= oldest_tick) & (Flag.tick <= latest_tick) & (Flag.status == 'accepted') & (Flag.exploit != 'manual')) \ 585 | .group_by(Flag.player, Flag.exploit, Flag.tick) 586 | results = [(result.player, result.exploit, result.tick, result.flag_count) for result in query] 587 | 588 | tick_indices = [i for i in range(oldest_tick, latest_tick + 1)] 589 | report = {'ticks': tick_indices, 'exploits': {}} 590 | 591 | for player, exploit, tick, flag_count in results: 592 | key = f'{player}-{exploit}' 593 | if key not in report['exploits']: 594 | report['exploits'][key] = { 595 | 'player': player, 596 | 'exploit': exploit, 597 | 'data': { 598 | 'accepted': [0] * len(tick_indices) 599 | } 600 | } 601 | 602 | report['exploits'][key]['data']['accepted'][tick_indices.index(tick)] = flag_count 603 | 604 | return report 605 | 606 | 607 | def setup_database(log=True): 608 | postgres = PostgresqlDatabase( 609 | config['database']['name'], 610 | user=config['database']['user'], 611 | password=config['database']['password'], 612 | host=config['database']['host'], 613 | port=config['database']['port'] 614 | ) 615 | 616 | db.initialize(postgres) 617 | try: 618 | db.connect() 619 | except Exception as e: 620 | logger.error( 621 | f"An error occurred when connecting to the database:\n{st.color(e, 'red')}") 622 | exit(1) 623 | 624 | db.create_tables([Flag]) 625 | Flag.add_index(Flag.value) 626 | 627 | if log: 628 | logger.success('Database connected.') 629 | 630 | 631 | def configure_flask(): 632 | # Configure CORS 633 | if os.environ.get('PYTHON_ENV') == 'development': 634 | CORS(app) 635 | 636 | # Set static path 637 | app.static_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web', 'dist') 638 | 639 | # Disable logs 640 | logging.getLogger('werkzeug').setLevel(logging.ERROR) 641 | 642 | 643 | def submit_func(flags): 644 | module_name = config['submitter']['module'] 645 | imported_module = reload(import_module(module_name)) # Ensure it's always the latest one 646 | imported_func = getattr(imported_module, 'submit') 647 | 648 | return imported_func(flags) 649 | 650 | 651 | def load_config(): 652 | # Load server.yaml 653 | if not os.path.isfile('server.yaml'): 654 | logger.error(f"{st.bold('server.yaml')} not found in the current working directory. Exiting...") 655 | exit(1) 656 | 657 | with open('server.yaml', 'r') as file: 658 | yaml_data = yaml.safe_load(file) 659 | 660 | if not yaml_data: 661 | logger.error(f"{st.bold('fast.yaml')} is empty. Exiting...") 662 | exit(1) 663 | 664 | # Load and validate server config 665 | if not validate_data(yaml_data, server_yaml_schema, custom=validate_delay): 666 | logger.error(f"Fix errors in {st.bold('server.yaml')} and rerun.") 667 | exit(1) 668 | 669 | deep_update(config, yaml_data) 670 | 671 | # Wrap single team ip in a list 672 | if type(config['game']['team_ip']) != list: 673 | config['game']['team_ip'] = [config['game']['team_ip']] 674 | 675 | if config['server'].get('password'): 676 | conn_str = f"http://username:***@{config['server']['host']}:{config['server']['port']}" 677 | else: 678 | conn_str = f"http://{config['server']['host']}:{config['server']['port']}" 679 | 680 | logger.success(f'Fast server configured successfully.') 681 | logger.info(f'Server will run at {st.color(conn_str, "cyan")}.') 682 | 683 | 684 | def recover(): 685 | global tick_start, tick_number, server_start 686 | 687 | if os.path.isfile(RECOVERY_CONFIG_PATH): 688 | with open(RECOVERY_CONFIG_PATH) as file: 689 | recovery_data = json.loads(file.read()) 690 | 691 | now = datetime.now() 692 | server_start = datetime.fromtimestamp(float(recovery_data['started'])) 693 | tick_duration = timedelta(seconds=config['game']['tick_duration']) 694 | 695 | ticks_passed = (now - server_start) // tick_duration 696 | into_tick = (now - server_start) % tick_duration 697 | 698 | tick_start = now + tick_duration - into_tick 699 | tick_number = ticks_passed 700 | 701 | logger.info(f"Continuing from tick {st.bold(tick_number)}. Tick scheduled for {st.bold(tick_start.strftime('%H:%M:%S'))}. ⏱️") 702 | logger.info(f"To reset Fast and run from tick 0, run {st.bold('reset')} and rerun.") 703 | else: 704 | tick_start = server_start = datetime.now() + timedelta(seconds=0.5) 705 | with open(RECOVERY_CONFIG_PATH, 'w') as file: 706 | file.write(json.dumps({ 707 | 'started': tick_start.timestamp(), 708 | })) 709 | 710 | 711 | def banner(): 712 | vers = '1.0.0' 713 | print(f""" 714 | \033[32;1m .___ ____\033[0m ______ __ 715 | \033[32;1m / /\__/ /\033[0m / ____/_ ____ / /_ 716 | \033[32;1m / / / ❬` \033[0m / /_/ __ `/ ___/ __/ 717 | \033[32;1m /___/ /____\ \033[0m / __/ /_/ (__ ) /_ 718 | \033[32;1m / \___\/ \033[0m/_/ \__,_/____/\__/ 719 | \033[32;1m/\033[0m \033[32mserver\033[0m \033[2mv{vers}\033[0m 720 | """) 721 | -------------------------------------------------------------------------------- /docs/assets/demos/client.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 125, "height": 24, "timestamp": 1692462040, "env": {"SHELL": "/bin/bash", "TERM": "screen-256color"}} 2 | [0.0, "o", "\u001b[01;32ms4ndu@player\u001b[00m:\u001b[01;34m~/CTF/FastDemo\u001b[00m$ "] 3 | [1.044329, "o", "f"] 4 | [1.115126, "o", "a"] 5 | [1.195967, "o", "s"] 6 | [1.307525, "o", "t"] 7 | [1.835475, "o", "\r\n"] 8 | [2.681749, "o", "\r\n\u001b[34;1m .___ ____\u001b[0m ______ __ \r\n\u001b[34;1m / /\\__/ /\u001b[0m / ____/_ ____ / /_ \r\n\u001b[34;1m / / / ❬` \u001b[0m / /_/ __ `/ ___/ __/\r\n\u001b[34;1m /___/ /____\\ \u001b[0m / __/ /_/ (__ ) /_ \r\n\u001b[34;1m / \\___\\/ \u001b[0m/_/ \\__,_/____/\\__/ \r\n\u001b[34;1m/\u001b[0m \u001b[34mclient\u001b[0m \u001b[2mv1.0.0\u001b[0m\r\n\r\n"] 9 | [2.685426, "o", "\u001b[2m18:20:43\u001b[0m \u001b[1m INFO |\u001b[0m Loading connection config...\r\n"] 10 | [2.692104, "o", "\u001b[2m18:20:43\u001b[0m \u001b[1m INFO |\u001b[0m Checking exploits config...\r\n"] 11 | [2.716224, "o", "\u001b[2m18:20:43\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m No errors found in exploits config.\r\n\u001b[2m18:20:43\u001b[0m \u001b[1m INFO |\u001b[0m Fetching game config...\r\n"] 12 | [2.716545, "o", "\u001b[2m18:20:43\u001b[0m \u001b[1m INFO |\u001b[0m Connecting to \u001b[36mhttp://s4ndu:***@fast.example.com:2023\u001b[0m\r\n"] 13 | [2.725697, "o", "\u001b[2m18:20:43\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m Game configured successfully. — \u001b[2m20s tick, DEMO[A-Za-z0-9+\\/=]{48}, 10.1.5.1\u001b[0m\r\n"] 14 | [2.73156, "o", "\u001b[2m18:20:43\u001b[0m \u001b[1m INFO |\u001b[0m Synchronizing with the server... Tick will start at \u001b[1m18:21:00\u001b[0m.\r\n"] 15 | [19.953546, "o", "\u001b[2m18:21:00\u001b[0m \u001b[1m INFO |\u001b[0m Reloading exploits...\r\n"] 16 | [19.983095, "o", "\u001b[2m18:21:00\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m Loaded \u001b[1m3\u001b[0m exploits.\r\n"] 17 | [19.983659, "o", "\u001b[2m18:21:00\u001b[0m \u001b[1m INFO |\u001b[0m Running \u001b[1malpha\u001b[0m at \u001b[1m17\u001b[0m targets...\r\n"] 18 | [19.984598, "o", "\u001b[2m18:21:00\u001b[0m \u001b[1m INFO |\u001b[0m Running \u001b[1mbravo\u001b[0m at \u001b[1m17\u001b[0m targets...\r\n"] 19 | [19.986262, "o", "\u001b[2m18:21:00\u001b[0m \u001b[1m INFO |\u001b[0m Running \u001b[1mcharlie\u001b[0m at \u001b[1m17\u001b[0m targets...\r\n"] 20 | [20.412002, "o", "\u001b[2m18:21:01\u001b[0m \u001b[1m INFO |\u001b[0m Running batch 1/3 of \u001b[1mbravo\u001b[0m at \u001b[1m6\u001b[0m targets.\r\n"] 21 | [20.456234, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.18.1\u001b[0m. 🚩 — \u001b[2mDEMOASJL9esuYMf8k9KYVi5Kg2VrdIc9pvvWDo0y4NMVv4P...\u001b[0m\r\n"] 22 | [20.600237, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m19\u001b[0m new flags, and \u001b[1m11\u001b[0m duplicate flags from \u001b[1m10.1.6.1\u001b[0m. 🚩 — \u001b[2mDEMO791IGDSikEjWJ7iomjyiAkKRYrTUor8vg10zCnjE9ol...\u001b[0m\r\n"] 23 | [20.687443, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m14\u001b[0m new flags, and \u001b[1m16\u001b[0m duplicate flags from \u001b[1m10.1.17.1\u001b[0m. 🚩 — \u001b[2mDEMOFgejCJ8/9tqHYeP+zB0Aey00rJ1dboQAHnGnGFZVrOE...\u001b[0m\r\n"] 24 | [20.689125, "o", "\u001b[2m18:21:01\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1malpha\u001b[0m retrieved own flag! Patch the service ASAP.\r\n"] 25 | [20.765454, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m9\u001b[0m new flags, and \u001b[1m21\u001b[0m duplicate flags from \u001b[1m10.1.4.1\u001b[0m. 🚩 — \u001b[2mDEMOpwlE6GAJQWCRHlrlt7b6Cu1EFBXiATQVO0aJ5IJ8mCW...\u001b[0m\r\n"] 26 | [20.846157, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m7\u001b[0m new flags, and \u001b[1m23\u001b[0m duplicate flags from \u001b[1m10.1.2.1\u001b[0m. 🚩 — \u001b[2mDEMOzatZB97iF0Z0lb2HoXEGQLGn/FCQU3yh4MKdp5JKfaU...\u001b[0m\r\n"] 27 | [20.84755, "o", "\u001b[2m18:21:01\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1mbravo\u001b[0m retrieved own flag! Patch the service ASAP.\r\n"] 28 | [20.925511, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.3.1\u001b[0m. 🚩 — \u001b[2mDEMOovkXCXqxlDsqJHZ8ZyKdvIs/NAFpC6NRaE251hMCIqd...\u001b[0m\r\n"] 29 | [20.946986, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.8.1\u001b[0m. 🚩 — \u001b[2mDEMOX9RP7TNRp7MLGZ5MeVJR+iCvw7JUqCLLk+3Nc5/qqTq...\u001b[0m\r\n"] 30 | [20.966871, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.15.1\u001b[0m. 🚩 — \u001b[2mDEMOBSBnaeYYFs7besvGh0Baff+eWJWV5+Qjt68cDmVy9Cj...\u001b[0m\r\n"] 31 | [20.985303, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.3.1\u001b[0m. 🚩 — \u001b[2mDEMOAt5qowP5r3e1bcbmtcTsOwOvUxGfjFRSoBqZKVc2uAA...\u001b[0m\r\n"] 32 | [21.003205, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.17.1\u001b[0m. 🚩 — \u001b[2mDEMOer16u7f7M0uSCUE2y4Fp9hO6tiZEBRTNa7veGIPAV/t...\u001b[0m\r\n"] 33 | [21.023224, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.2.1\u001b[0m. 🚩 — \u001b[2mDEMO2dD9pXWuvZXXTZkH4W/XF3nMi13ulh/DFa1rbAZXGbW...\u001b[0m\r\n"] 34 | [21.039366, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.7.1\u001b[0m. 🚩 — \u001b[2mDEMO6QRk2SuM/BZ3fVf6AdJOadJWeHb5NtqB3yhfUaFOOTV...\u001b[0m\r\n"] 35 | [21.057801, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.10.1\u001b[0m. 🚩 — \u001b[2mDEMO2jKDLoU9uOLdhrEqMFMyoTUjPAFdVfrRPlE6+oFsyyt...\u001b[0m\r\n"] 36 | [21.078356, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.14.1\u001b[0m. 🚩 — \u001b[2mDEMOVBgxGhtdOg0Ljl6Xv31lOrSqTpJp9aJU3GggQphUDe5...\u001b[0m\r\n"] 37 | [21.100132, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.11.1\u001b[0m. 🚩 — \u001b[2mDEMOcEkIv996FyAHvrPJVqdnK39WQWniLRKES5DofL1t+CJ...\u001b[0m\r\n"] 38 | [21.1226, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.6.1\u001b[0m. 🚩 — \u001b[2mDEMO3Kmoc8iXXK1k7LdEJBBH9YZRauwzw7OoCs6Dt/3LbiH...\u001b[0m\r\n"] 39 | [21.143595, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.4.1\u001b[0m. 🚩 — \u001b[2mDEMOfik043CNaF2mrbmjxv/c1npegIqQjRM9ECOYq1Xkgbi...\u001b[0m\r\n"] 40 | [21.161838, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.16.1\u001b[0m. 🚩 — \u001b[2mDEMOOIwXf7Pov18M+8uOmZZoAa5DBEmqfkx9RkJVFVg2zCU...\u001b[0m\r\n"] 41 | [21.183777, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.12.1\u001b[0m. 🚩 — \u001b[2mDEMOpdU/j00e7Gs28KcDXQ/71t9r5Jh7rwr6+xBH0pOQ/Sd...\u001b[0m\r\n"] 42 | [21.201317, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.2.1\u001b[0m. 🚩 — \u001b[2mDEMOTeH/8dJ5lAMgvBmoTur+rWFkpoIVgnYZMV0f7Shkx4c...\u001b[0m\r\n"] 43 | [21.219878, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.9.1\u001b[0m. 🚩 — \u001b[2mDEMOIl6EXCCMkGtWVg6mEOgljrbmMt1KMhVb0zt939yIAyC...\u001b[0m\r\n"] 44 | [21.238337, "o", "\u001b[2m18:21:01\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.6.1\u001b[0m. 🚩 — \u001b[2mDEMOnAjjQF+uMeupupVKx90A78ogIycHavN97/Ergw219Sh...\u001b[0m\r\n"] 45 | [21.256797, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.13.1\u001b[0m. 🚩 — \u001b[2mDEMOEDMCDDkqM+X6ooYT8SS8ZF1gVJ6MAaMmdvg7ARcS0N0...\u001b[0m\r\n"] 46 | [21.277292, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.3.1\u001b[0m. 🚩 — \u001b[2mDEMOxiXC5HI+qN0MpxBxPC6WTz7UATU8OjuXDydwYd3/+na...\u001b[0m\r\n"] 47 | [21.279584, "o", "\u001b[2m18:21:02\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved own flag! Patch the service ASAP.\r\n"] 48 | [21.299682, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.8.1\u001b[0m. 🚩 — \u001b[2mDEMO+eVALQ8oJ4LJajFgdezzEJliVXvHiPOLZsvntNLUSeY...\u001b[0m\r\n"] 49 | [21.32058, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.7.1\u001b[0m. 🚩 — \u001b[2mDEMOb4mzoFISF8kOY1X3R+hjaMKYuyHqF7A+AIQ5mwdPkw4...\u001b[0m\r\n"] 50 | [21.342266, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.4.1\u001b[0m. 🚩 — \u001b[2mDEMOHBtBhnZmx3++Zlp3tVMJfrJXj9wO0n8K4Vk6qX1OkUr...\u001b[0m\r\n"] 51 | [21.361257, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.9.1\u001b[0m. 🚩 — \u001b[2mDEMON7ZhOxxXILwCdql66kmb1WwVg0oTHW4/EXhP2AWXRVp...\u001b[0m\r\n"] 52 | [21.370192, "o", "\u001b[2m18:21:02\u001b[0m \u001b[1m INFO |\u001b[0m \u001b[1malpha\u001b[0m finished.\r\n"] 53 | [21.379992, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.12.1\u001b[0m. 🚩 — \u001b[2mDEMOZXkeRqouPBhPzsv0Wo6jp2Onh8bm0j5cO94NYPjNbGX...\u001b[0m\r\n"] 54 | [21.398694, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.11.1\u001b[0m. 🚩 — \u001b[2mDEMOLM0SN1sntyrcCYvOWM9ih+FY9YVO/jvfCNbazRtnsTE...\u001b[0m\r\n"] 55 | [21.416444, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.16.1\u001b[0m. 🚩 — \u001b[2mDEMOUUS27XwrJ4cHV35xAfZLerhSyE9wRiqRnhzHNMStqH8...\u001b[0m\r\n"] 56 | [21.43575, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.10.1\u001b[0m. 🚩 — \u001b[2mDEMOaymYNVuhJKtxwkwtxZHt0HikhaJFsCoQOvNOI36NvD8...\u001b[0m\r\n"] 57 | [21.455535, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.14.1\u001b[0m. 🚩 — \u001b[2mDEMOaH5WNFF/TTiaQSS2szUaQgNc3YFfvtxeLH4qqi9P3XO...\u001b[0m\r\n"] 58 | [21.475315, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.15.1\u001b[0m. 🚩 — \u001b[2mDEMOKHmOqr5DcjDzcrwdVvN5rWqZfGzEg2jL09S/hirnUsX...\u001b[0m\r\n"] 59 | [21.495538, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.13.1\u001b[0m. 🚩 — \u001b[2mDEMOki/CpCND7SfUrZIf5MiSMIOl+Nf6f+2/JIAMMPKONQ9...\u001b[0m\r\n"] 60 | [21.516015, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.17.1\u001b[0m. 🚩 — \u001b[2mDEMOjaUBEH7uQPlrbEQZ79zjT6CrRi33tJ4QKlhdjfZwWSS...\u001b[0m\r\n"] 61 | [21.538974, "o", "\u001b[2m18:21:02\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.18.1\u001b[0m. 🚩 — \u001b[2mDEMOrTgo+O+xjopSgn+1AGseRW+tgSTmIfTPda/Prf9Lcb2...\u001b[0m\r\n"] 62 | [21.65296, "o", "\u001b[2m18:21:02\u001b[0m \u001b[1m INFO |\u001b[0m \u001b[1mcharlie\u001b[0m finished.\r\n"] 63 | [22.421845, "o", "\u001b[2m18:21:03\u001b[0m \u001b[1m INFO |\u001b[0m Running batch 2/3 of \u001b[1mbravo\u001b[0m at \u001b[1m6\u001b[0m targets.\r\n"] 64 | [22.530176, "o", "\u001b[2m18:21:03\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.9.1\u001b[0m. 🚩 — \u001b[2mDEMOhLKljjqdsPgtca7J7MWQf9EnK1SnJti5Hu6RidFt059...\u001b[0m\r\n"] 65 | [22.608629, "o", "\u001b[2m18:21:03\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.8.1\u001b[0m. 🚩 — \u001b[2mDEMOoJilTUpq6bWhSGyIReIFC681nk1qitPA6aZFjtG8tcA...\u001b[0m\r\n"] 66 | [22.688296, "o", "\u001b[2m18:21:03\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.7.1\u001b[0m. 🚩 — \u001b[2mDEMOM10PiOJrT0aGBPRCfFI4bXLh90wOfMeATj1MFNwsKWe...\u001b[0m\r\n"] 67 | [22.767249, "o", "\u001b[2m18:21:03\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m6\u001b[0m new flags, and \u001b[1m24\u001b[0m duplicate flags from \u001b[1m10.1.10.1\u001b[0m. 🚩 — \u001b[2mDEMO3i8qVlTJ8aTEnAd0CqfsDFTZcCrb2/J7FJzWsFrm6Gi...\u001b[0m\r\n"] 68 | [22.850133, "o", "\u001b[2m18:21:03\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.11.1\u001b[0m. 🚩 — \u001b[2mDEMOdDA+Bbn3w1Pqe7nG3ob21HuW3qcRRZlOdnwSdYJ8sH9...\u001b[0m\r\n"] 69 | [22.932891, "o", "\u001b[2m18:21:03\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.18.1\u001b[0m. 🚩 — \u001b[2mDEMOD8e4hBextO4iJ6RAugyN75PBeBtY70XXn6J9+b/BaVz...\u001b[0m\r\n"] 70 | [24.451223, "o", "\u001b[2m18:21:05\u001b[0m \u001b[1m INFO |\u001b[0m Running batch 3/3 of \u001b[1mbravo\u001b[0m at \u001b[1m5\u001b[0m targets.\r\n"] 71 | [24.562837, "o", "\u001b[2m18:21:05\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.13.1\u001b[0m. 🚩 — \u001b[2mDEMOi7jZyjEPA0vbEOQKg3pGicElR6FmFP9hGnjWFVLGPeQ...\u001b[0m\r\n"] 72 | [24.640372, "o", "\u001b[2m18:21:05\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.15.1\u001b[0m. 🚩 — \u001b[2mDEMOkhQQJ7GftGIu+UYJdei8Bx+WZSBM3gh1PdZBrczZSmo...\u001b[0m\r\n"] 73 | [24.723794, "o", "\u001b[2m18:21:05\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.14.1\u001b[0m. 🚩 — \u001b[2mDEMOIU3ZWq9m0qrMmmko3zQS6a95XZiNBWUjk3YjOW8gRFC...\u001b[0m\r\n"] 74 | [24.807156, "o", "\u001b[2m18:21:05\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.12.1\u001b[0m. 🚩 — \u001b[2mDEMOEfTLIiosCE3PiwE6Sl3znwTlUs2MghXpDyfYM+8UfJe...\u001b[0m\r\n"] 75 | [24.89251, "o", "\u001b[2m18:21:05\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.16.1\u001b[0m. 🚩 — \u001b[2mDEMOA8Y2EMt0voSZIeZWfN2zEwVKC35MoJ6pa44KPGcWGti...\u001b[0m\r\n"] 76 | [24.928269, "o", "\u001b[2m18:21:05\u001b[0m \u001b[1m INFO |\u001b[0m \u001b[1mbravo\u001b[0m finished.\r\n"] 77 | [39.965649, "o", "\u001b[2m18:21:20\u001b[0m \u001b[1m INFO |\u001b[0m Running \u001b[1malpha\u001b[0m at \u001b[1m17\u001b[0m targets...\r\n"] 78 | [39.969251, "o", "\u001b[2m18:21:20\u001b[0m \u001b[1m INFO |\u001b[0m Running \u001b[1mbravo\u001b[0m at \u001b[1m17\u001b[0m targets...\r\n"] 79 | [39.969837, "o", "\u001b[2m18:21:20\u001b[0m \u001b[1m INFO |\u001b[0m Running \u001b[1mcharlie\u001b[0m at \u001b[1m17\u001b[0m targets...\r\n"] 80 | [40.250784, "o", "\u001b[2m18:21:21\u001b[0m \u001b[1m INFO |\u001b[0m Running batch 1/3 of \u001b[1mbravo\u001b[0m at \u001b[1m6\u001b[0m targets.\r\n"] 81 | [40.353547, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.3.1\u001b[0m. 🚩 — \u001b[2mDEMOQsw0zUDGHfANyvat7stie12+JmGCCurBvIY+u38HkLz...\u001b[0m\r\n"] 82 | [40.424466, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.2.1\u001b[0m. 🚩 — \u001b[2mDEMO/MBEiemdAQjppyk3leRS/dkM3LtUv8NN5Rl2h+uQ2Nk...\u001b[0m\r\n"] 83 | [40.502387, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.4.1\u001b[0m. 🚩 — \u001b[2mDEMOfPqeJgSNyMHp85uFoFatdKR6dGBNKBQwZ8I92m0T6Yg...\u001b[0m\r\n"] 84 | [40.575183, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.6.1\u001b[0m. 🚩 — \u001b[2mDEMO2RErNIliVqb6VMQYsNkSD2C2PUXU5+Isp/tA4s+Qg/V...\u001b[0m\r\n"] 85 | [40.5983, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.2.1\u001b[0m. 🚩 — \u001b[2mDEMOI4Ak3l+jGYgMAdES7FSVJNzXomxd5Yy5o+oY/Ce6BtT...\u001b[0m\r\n"] 86 | [40.684022, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.17.1\u001b[0m. 🚩 — \u001b[2mDEMOw1CRLxAnvxrfvgayI3RC7LB8pMS3imAoZPEWx7ltLs3...\u001b[0m\r\n"] 87 | [40.68583, "o", "\u001b[2m18:21:21\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1mbravo\u001b[0m retrieved own flag! Patch the service ASAP.\r\n"] 88 | [40.707122, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.3.1\u001b[0m. 🚩 — \u001b[2mDEMOLvqh6rQMp9YJIRIqlfbCtr2yxuBI155XuxWs9frx0vE...\u001b[0m\r\n"] 89 | [40.709307, "o", "\u001b[2m18:21:21\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved own flag! Patch the service ASAP.\r\n"] 90 | [40.729231, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.4.1\u001b[0m. 🚩 — \u001b[2mDEMOav3QI/LEC/DfLJ9t7Wmw8acvOahbEjNwHQpiHREfWkz...\u001b[0m\r\n"] 91 | [40.750256, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.4.1\u001b[0m. 🚩 — \u001b[2mDEMOkcBGWFy70m/9LmxblSQbafusy644pqJciqL5wnT5Fnm...\u001b[0m\r\n"] 92 | [40.771653, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.6.1\u001b[0m. 🚩 — \u001b[2mDEMO2DMNRGIquCZ03mBy16Fc3KjZXAN3iUjrJ//WkMMix4j...\u001b[0m\r\n"] 93 | [40.791456, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.3.1\u001b[0m. 🚩 — \u001b[2mDEMOmG+BuJX8JzrFhOi4zlRK/ETrkX5Dw6kfOsxHhYq/IfH...\u001b[0m\r\n"] 94 | [40.810428, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.8.1\u001b[0m. 🚩 — \u001b[2mDEMO7ON8WiD7XR5xrKmvhC91NHO9W7V62++ijFSsekTMoaW...\u001b[0m\r\n"] 95 | [40.828227, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.9.1\u001b[0m. 🚩 — \u001b[2mDEMOz6Uh5Aioz30ELnDTtUrSSh81TGcrZE0qGM8eIt3z8+T...\u001b[0m\r\n"] 96 | [40.849368, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.9.1\u001b[0m. 🚩 — \u001b[2mDEMORp6cOb7yPwRzRrugpMQsxwsTvIuZL6FlZkkJUruDhB4...\u001b[0m\r\n"] 97 | [40.870044, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.7.1\u001b[0m. 🚩 — \u001b[2mDEMOz0e+maT8TNv83CvGI64i+v7lLQ8r5fRY6XTvNGdy32I...\u001b[0m\r\n"] 98 | [40.887412, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.10.1\u001b[0m. 🚩 — \u001b[2mDEMOUTWBYFywpE6FNZ7RL1dwMW52hGaVz6WBCQrlpyF2O3O...\u001b[0m\r\n"] 99 | [40.912084, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.2.1\u001b[0m. 🚩 — \u001b[2mDEMOOtXWk7I89HHMLLMf6X8I2cKew6RK/BVKJ/cQi3bMlvD...\u001b[0m\r\n"] 100 | [40.931351, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.6.1\u001b[0m. 🚩 — \u001b[2mDEMOwH21IP9TS6PROMfBUNp95nyZ3PBQYyc5DSiQaeLS/JS...\u001b[0m\r\n"] 101 | [40.950687, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.8.1\u001b[0m. 🚩 — \u001b[2mDEMO+Q96RVMlbDF6DVz+WrTLSuujwumG2X6pmd2HJ990oGa...\u001b[0m\r\n"] 102 | [40.971182, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.13.1\u001b[0m. 🚩 — \u001b[2mDEMOAiyhdJqoCHDoVigppBw5QeXvqhdj/CCRLiT85EGPm2Z...\u001b[0m\r\n"] 103 | [40.992297, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.11.1\u001b[0m. 🚩 — \u001b[2mDEMOaTMrfqVHJ2hOHz1pXN/+pNsOAWsJJtTr3qFRmpec51p...\u001b[0m\r\n"] 104 | [41.012243, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.12.1\u001b[0m. 🚩 — \u001b[2mDEMOE9zZoSufmmeJEsAv8lUxwGuSz/nRbsAca6uPxAm0agb...\u001b[0m\r\n"] 105 | [41.032615, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.10.1\u001b[0m. 🚩 — \u001b[2mDEMOiVwCliwknFtC7dBRDpI8kly4DNsGTl/oHRbKSQFGE3p...\u001b[0m\r\n"] 106 | [41.050997, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.15.1\u001b[0m. 🚩 — \u001b[2mDEMOKW4NGhxYgJ9oRZJKsR1p0HgJc56GHqA1pnjWNz8op0o...\u001b[0m\r\n"] 107 | [41.053337, "o", "\u001b[2m18:21:21\u001b[0m \u001b[33m\u001b[1m WARNING |\u001b[0m \u001b[1malpha\u001b[0m retrieved own flag! Patch the service ASAP.\r\n"] 108 | [41.072442, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.7.1\u001b[0m. 🚩 — \u001b[2mDEMOxpZsb1CDGtBa9w/RPkD5l9fkGGnTE+t0Nz+qZJCiCMa...\u001b[0m\r\n"] 109 | [41.092932, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.14.1\u001b[0m. 🚩 — \u001b[2mDEMOqy1ASzgTpvb4koOIhrhuIh01lLrqtoXu7AAyr8sgIya...\u001b[0m\r\n"] 110 | [41.114274, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.11.1\u001b[0m. 🚩 — \u001b[2mDEMOeBYCKsSXboJA8SEpFWIBgSdJWhDc9zUe3qaiZx9F55f...\u001b[0m\r\n"] 111 | [41.134901, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.13.1\u001b[0m. 🚩 — \u001b[2mDEMODlQVwxSB+s+thZjzWt0TFnE8WXDWRQaaEhOUu4ulcEJ...\u001b[0m\r\n"] 112 | [41.1533, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.14.1\u001b[0m. 🚩 — \u001b[2mDEMOYPPcxhUwR8G1PllTlaQJDlVtuyl4GYlSRiLfRxm/v5k...\u001b[0m\r\n"] 113 | [41.171597, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.17.1\u001b[0m. 🚩 — \u001b[2mDEMO48tqBGQrCY+sXDxvfhdg9D1vYI4kSa0YMreVxPsuFbJ...\u001b[0m\r\n"] 114 | [41.192091, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.12.1\u001b[0m. 🚩 — \u001b[2mDEMObgwCNL9GGuOry7kbtAXovETlEAWfRd6HstGBFc/mvku...\u001b[0m\r\n"] 115 | [41.213112, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.15.1\u001b[0m. 🚩 — \u001b[2mDEMO5Htv/cwLGmnQ7mURIdLNItM6VcpRGQZcHKfhrrP9UUB...\u001b[0m\r\n"] 116 | [41.23484, "o", "\u001b[2m18:21:21\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.17.1\u001b[0m. 🚩 — \u001b[2mDEMO8Ql6ePA4+jeI5BqMYOAuIYtSsVEnosVkGlYxALoeXZS...\u001b[0m\r\n"] 117 | [41.253736, "o", "\u001b[2m18:21:22\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.16.1\u001b[0m. 🚩 — \u001b[2mDEMOJBDf8WlpeqlT0UYTLK5Hvc1pAP0CUOA/7AQd4IGCHzS...\u001b[0m\r\n"] 118 | [41.274523, "o", "\u001b[2m18:21:22\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.16.1\u001b[0m. 🚩 — \u001b[2mDEMOcZt/L9b6ci+srIo31g+kT2j+XIqRZwKwWEttINa7rOi...\u001b[0m\r\n"] 119 | [41.296525, "o", "\u001b[2m18:21:22\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mcharlie\u001b[0m retrieved a new flag from \u001b[1m10.1.18.1\u001b[0m. 🚩 — \u001b[2mDEMOpvGt7BuwJ5td3jB/WWpH9Mm6TXWpt60mqD6dn2jB8e9...\u001b[0m\r\n"] 120 | [41.315099, "o", "\u001b[2m18:21:22\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1malpha\u001b[0m retrieved a new flag from \u001b[1m10.1.18.1\u001b[0m. 🚩 — \u001b[2mDEMOTObN3hPm8EPJGNJwtvFzah2f3o6tGz8u1RauZ5y4YGO...\u001b[0m\r\n"] 121 | [41.42698, "o", "\u001b[2m18:21:22\u001b[0m \u001b[1m INFO |\u001b[0m \u001b[1malpha\u001b[0m finished.\r\n"] 122 | [41.432303, "o", "\u001b[2m18:21:22\u001b[0m \u001b[1m INFO |\u001b[0m \u001b[1mcharlie\u001b[0m finished.\r\n"] 123 | [42.279318, "o", "\u001b[2m18:21:23\u001b[0m \u001b[1m INFO |\u001b[0m Running batch 2/3 of \u001b[1mbravo\u001b[0m at \u001b[1m6\u001b[0m targets.\r\n"] 124 | [42.381892, "o", "\u001b[2m18:21:23\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.8.1\u001b[0m. 🚩 — \u001b[2mDEMOBX+Z2yppY35huikY6q4nGDXd8YShQVDz9f1/iJrOqSr...\u001b[0m\r\n"] 125 | [42.463517, "o", "\u001b[2m18:21:23\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.10.1\u001b[0m. 🚩 — \u001b[2mDEMOlocYfdG4jQv9RyT+qTfKnQO7fVf8ZxUHav77zHQpvy1...\u001b[0m\r\n"] 126 | [42.543168, "o", "\u001b[2m18:21:23\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.7.1\u001b[0m. 🚩 — \u001b[2mDEMOfQnjOC3LWt5OdlnWMx7GO2iiomk7YKw0gq+CwME4H8F...\u001b[0m\r\n"] 127 | [42.619744, "o", "\u001b[2m18:21:23\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.9.1\u001b[0m. 🚩 — \u001b[2mDEMOo2Q7rfFjJXtvZbeRbjRKau17a2gf7kqBPy18eeaAfjF...\u001b[0m\r\n"] 128 | [42.700278, "o", "\u001b[2m18:21:23\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.11.1\u001b[0m. 🚩 — \u001b[2mDEMOQvoQJ748NfAEb1e6t/WAfI0rSL34JIyplfg/NSBJLsW...\u001b[0m\r\n"] 129 | [42.783648, "o", "\u001b[2m18:21:23\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.18.1\u001b[0m. 🚩 — \u001b[2mDEMOUaWR9cYyy7saVL/mjNaTHG5tjAY0UxLNv8V7/krcEWW...\u001b[0m\r\n"] 130 | [44.311352, "o", "\u001b[2m18:21:25\u001b[0m \u001b[1m INFO |\u001b[0m Running batch 3/3 of \u001b[1mbravo\u001b[0m at \u001b[1m5\u001b[0m targets.\r\n"] 131 | [44.422688, "o", "\u001b[2m18:21:25\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.13.1\u001b[0m. 🚩 — \u001b[2mDEMO0bST/MPhqMoCDrxErU88xpMf5cFiQpcQsDxIz3gEf1n...\u001b[0m\r\n"] 132 | [44.49913, "o", "\u001b[2m18:21:25\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.12.1\u001b[0m. 🚩 — \u001b[2mDEMOzqjVleMO7hTB/tyBNJ1poH3Vu8fyHLrqSWspttOrqgG...\u001b[0m\r\n"] 133 | [44.576051, "o", "\u001b[2m18:21:25\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.14.1\u001b[0m. 🚩 — \u001b[2mDEMOEdhGjIIquqLKLDYj9kshRXuRApRuIxoqjDVEQm+mLhV...\u001b[0m\r\n"] 134 | [44.662781, "o", "\u001b[2m18:21:25\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.16.1\u001b[0m. 🚩 — \u001b[2mDEMOygkBdTkFj3WGxrejLtE0TNtI9TI/Z+49TcanYdfz/Pp...\u001b[0m\r\n"] 135 | [44.737157, "o", "\u001b[2m18:21:25\u001b[0m \u001b[32m\u001b[1m SUCCESS |\u001b[0m \u001b[1mbravo\u001b[0m retrieved \u001b[1m5\u001b[0m new flags, and \u001b[1m25\u001b[0m duplicate flags from \u001b[1m10.1.15.1\u001b[0m. 🚩 — \u001b[2mDEMOzsXT5I6BL/Pc3b2L+1urqMWXpLJvH1Vw9O48dKlajz6...\u001b[0m\r\n"] 136 | [44.776762, "o", "\u001b[2m18:21:25\u001b[0m \u001b[1m INFO |\u001b[0m \u001b[1mbravo\u001b[0m finished.\r\n"] --------------------------------------------------------------------------------