postgres version
This commit is contained in:
26
docker-compose.farm.yaml
Normal file
26
docker-compose.farm.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
# Application service
|
||||||
|
farm:
|
||||||
|
build:
|
||||||
|
context: ./server/
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
# networks:
|
||||||
|
# - security_net
|
||||||
|
restart: "always"
|
||||||
|
ports:
|
||||||
|
- "${FARM_PORT}:8000"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:18
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: farm
|
||||||
|
POSTGRES_PASSWORD: asdasdasd
|
||||||
|
POSTGRES_DB: farmdb
|
||||||
|
healthcheck:
|
||||||
|
test: pg_isready -U farm -d farmdb
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
9
server/Dockerfile
Normal file
9
server/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
|
COPY ./ /app/
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
CMD ["./start_server.sh"]
|
||||||
14
server/__init__.py
Normal file
14
server/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
for handler in app.logger.handlers:
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
import api
|
||||||
|
import views
|
||||||
34
server/api.py
Normal file
34
server/api.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from flask import request, jsonify
|
||||||
|
from __init__ import app
|
||||||
|
import auth, database, reloader
|
||||||
|
from models import FlagStatus
|
||||||
|
from spam import is_spam_flag
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/get_config')
|
||||||
|
@auth.api_auth_required
|
||||||
|
def get_config():
|
||||||
|
config = reloader.get_config()
|
||||||
|
return jsonify({key: value for key, value in config.items()
|
||||||
|
if 'PASSWORD' not in key and 'TOKEN' not in key})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/post_flags', methods=['POST'])
|
||||||
|
@auth.api_auth_required
|
||||||
|
def post_flags():
|
||||||
|
flags = request.get_json()
|
||||||
|
flags = [item for item in flags if not is_spam_flag(item['flag'])]
|
||||||
|
|
||||||
|
cur_time = round(time.time())
|
||||||
|
rows = [(item['flag'], item['sploit'], item['team'], cur_time, FlagStatus.QUEUED.name)
|
||||||
|
for item in flags]
|
||||||
|
|
||||||
|
db = database.get()
|
||||||
|
cursor = db.cursor()
|
||||||
|
cursor.executemany("INSERT OR IGNORE INTO flags (flag, sploit, team, time, status) "
|
||||||
|
"VALUES (%s, %s, %s, %s, %s)", rows)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return ''
|
||||||
34
server/auth.py
Normal file
34
server/auth.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from flask import request, Response
|
||||||
|
|
||||||
|
import reloader
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate():
|
||||||
|
return Response(
|
||||||
|
'Could not verify your access level for that URL. '
|
||||||
|
'You have to login with proper credentials', 401,
|
||||||
|
{'WWW-Authenticate': 'Basic realm="Login Required"'})
|
||||||
|
|
||||||
|
|
||||||
|
def auth_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
auth = request.cookies.get('password')
|
||||||
|
config = reloader.get_config()
|
||||||
|
if auth is None or auth != config['SERVER_PASSWORD']:
|
||||||
|
return authenticate()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def api_auth_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
config = reloader.get_config()
|
||||||
|
if config['ENABLE_API_AUTH']:
|
||||||
|
if request.headers.get('X-Token', '') != config['API_TOKEN']:
|
||||||
|
return Response('Provided token is invalid.', 403)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated
|
||||||
|
|
||||||
45
server/config.py
Normal file
45
server/config.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
CONFIG = {
|
||||||
|
# Don't forget to remove the old database (flags.sqlite) before each competition.
|
||||||
|
|
||||||
|
'PORT': 8000,
|
||||||
|
|
||||||
|
# The clients will run sploits on TEAMS and
|
||||||
|
# fetch FLAG_FORMAT from sploits' stdout.
|
||||||
|
'TEAMS': {'Team #{}'.format(i): '10.0.0.{}'.format(i)
|
||||||
|
for i in range(1, 29 + 1)},
|
||||||
|
'FLAG_FORMAT': r'[A-Z0-9]{31}=',
|
||||||
|
|
||||||
|
# This configures how and where to submit flags.
|
||||||
|
# The protocol must be a module in protocols/ directory.
|
||||||
|
|
||||||
|
'SYSTEM_PROTOCOL': 'default',
|
||||||
|
'SYSTEM_HOST': '127.0.0.1',
|
||||||
|
'SYSTEM_PORT': 31337,
|
||||||
|
|
||||||
|
# 'SYSTEM_PROTOCOL': 'ructf_http',
|
||||||
|
# 'SYSTEM_URL': 'http://monitor.ructfe.org/flags',
|
||||||
|
# 'SYSTEM_TOKEN': 'your_secret_token',
|
||||||
|
|
||||||
|
# 'SYSTEM_PROTOCOL': 'volgactf',
|
||||||
|
# 'SYSTEM_HOST': '127.0.0.1',
|
||||||
|
|
||||||
|
# 'SYSTEM_PROTOCOL': 'forcad_tcp',
|
||||||
|
# 'SYSTEM_HOST': '127.0.0.1',
|
||||||
|
# 'SYSTEM_PORT': 31337,
|
||||||
|
# 'TEAM_TOKEN': 'your_secret_token',
|
||||||
|
|
||||||
|
# The server will submit not more than SUBMIT_FLAG_LIMIT flags
|
||||||
|
# every SUBMIT_PERIOD seconds. Flags received more than
|
||||||
|
# FLAG_LIFETIME seconds ago will be skipped.
|
||||||
|
'SUBMIT_FLAG_LIMIT': 50,
|
||||||
|
'SUBMIT_PERIOD': 5,
|
||||||
|
'FLAG_LIFETIME': 5 * 60,
|
||||||
|
|
||||||
|
# Password for the web interface. You can use it with any login.
|
||||||
|
# This value will be excluded from the config before sending it to farm clients.
|
||||||
|
'SERVER_PASSWORD': 'pepez_slit',
|
||||||
|
|
||||||
|
# Use authorization for API requests
|
||||||
|
'ENABLE_API_AUTH': False,
|
||||||
|
'API_TOKEN': '00000000000000000000'
|
||||||
|
}
|
||||||
201
server/database.py
Normal file
201
server/database.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"""
|
||||||
|
Module with PostgreSQL helpers
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import threading
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
|
||||||
|
|
||||||
|
schema_path = os.path.join(os.path.dirname(__file__), 'schema.sql')
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
db_host = os.getenv('DB_HOST', 'postgres')
|
||||||
|
db_port = os.getenv('DB_PORT', '5432')
|
||||||
|
db_name = os.getenv('DB_NAME', 'farmdb')
|
||||||
|
db_user = os.getenv('DB_USER', 'farm')
|
||||||
|
db_password = os.getenv('DB_PASSWORD', 'asdasdasd')
|
||||||
|
|
||||||
|
_init_started = False
|
||||||
|
_init_lock = threading.RLock()
|
||||||
|
|
||||||
|
|
||||||
|
def dict_factory(cursor, row):
|
||||||
|
"""
|
||||||
|
Convert database row to dictionary similar to sqlite3.Row
|
||||||
|
"""
|
||||||
|
return {col[0]: row[i] for i, col in enumerate(cursor.description)}
|
||||||
|
|
||||||
|
|
||||||
|
def get(context_bound=True):
|
||||||
|
"""
|
||||||
|
If there is no opened connection to the PostgreSQL database in the context
|
||||||
|
of the current request or if context_bound=False, get() opens a new
|
||||||
|
connection to the PostgreSQL database. Reopening the connection on each request
|
||||||
|
may have some overhead, but allows to avoid implementing a pool of
|
||||||
|
thread-local connections.
|
||||||
|
|
||||||
|
If the database schema needs initialization, get() creates and initializes it.
|
||||||
|
If get() is called from other threads at this time, they will wait
|
||||||
|
for the end of the initialization.
|
||||||
|
|
||||||
|
If context_bound=True, the connection will be closed after
|
||||||
|
request handling (when the context will be destroyed).
|
||||||
|
|
||||||
|
:returns: a connection to the initialized PostgreSQL database
|
||||||
|
"""
|
||||||
|
|
||||||
|
global _init_started
|
||||||
|
|
||||||
|
if context_bound and 'database' in g:
|
||||||
|
return g.database
|
||||||
|
|
||||||
|
# Connect to PostgreSQL database
|
||||||
|
database = psycopg2.connect(
|
||||||
|
host=db_host,
|
||||||
|
port=db_port,
|
||||||
|
dbname=db_name,
|
||||||
|
user=db_user,
|
||||||
|
password=db_password
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if initialization is needed
|
||||||
|
need_init = check_if_initialization_needed(database)
|
||||||
|
|
||||||
|
if need_init:
|
||||||
|
with _init_lock:
|
||||||
|
if not _init_started:
|
||||||
|
_init_started = True
|
||||||
|
_init(database)
|
||||||
|
|
||||||
|
if context_bound:
|
||||||
|
g.database = database
|
||||||
|
|
||||||
|
app.logger.info('DB connection established')
|
||||||
|
return database
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_initialization_needed(conn):
|
||||||
|
"""
|
||||||
|
Check if database needs initialization by looking for specific tables.
|
||||||
|
Modify this based on your application's requirements.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Check for existence of any table to see if DB is initialized
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
tables_exist = cursor.fetchone()[0]
|
||||||
|
cursor.close()
|
||||||
|
return not tables_exist
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Error checking initialization status: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _init(conn):
|
||||||
|
"""
|
||||||
|
Initialize the database schema and any required data.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Read and execute schema.sql if it exists
|
||||||
|
if os.path.exists(schema_path):
|
||||||
|
with open(schema_path, 'r') as f:
|
||||||
|
schema_sql = f.read()
|
||||||
|
cursor.execute(schema_sql)
|
||||||
|
app.logger.info("Executed schema.sql")
|
||||||
|
else:
|
||||||
|
# Fallback to basic tables if schema.sql doesn't exist
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS flags (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
flag TEXT UNIQUE NOT NULL,
|
||||||
|
team INTEGER NOT NULL,
|
||||||
|
tick INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS submissions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
flag TEXT NOT NULL,
|
||||||
|
team INTEGER NOT NULL,
|
||||||
|
tick INTEGER NOT NULL,
|
||||||
|
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
app.logger.info("Database initialized successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
app.logger.error(f"Database initialization failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def query(sql, args=()):
|
||||||
|
"""
|
||||||
|
Execute a query and return results as dictionaries
|
||||||
|
"""
|
||||||
|
conn = get()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(sql, args)
|
||||||
|
|
||||||
|
if cursor.description: # If it's a SELECT query
|
||||||
|
results = [dict_factory(cursor, row) for row in cursor.fetchall()]
|
||||||
|
else: # For INSERT, UPDATE, DELETE
|
||||||
|
results = None
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def execute(sql, args=()):
|
||||||
|
"""
|
||||||
|
Execute a query that doesn't return results (INSERT, UPDATE, DELETE)
|
||||||
|
"""
|
||||||
|
conn = get()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(sql, args)
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_one(sql, args=()):
|
||||||
|
"""
|
||||||
|
Execute a query and return first result as dictionary
|
||||||
|
"""
|
||||||
|
conn = get()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(sql, args)
|
||||||
|
|
||||||
|
if cursor.description: # If it's a SELECT query
|
||||||
|
row = cursor.fetchone()
|
||||||
|
result = dict_factory(cursor, row) if row else None
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def close(_):
|
||||||
|
if 'database' in g:
|
||||||
|
g.database.close()
|
||||||
13
server/models.py
Normal file
13
server/models.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class FlagStatus(Enum):
|
||||||
|
QUEUED = 0
|
||||||
|
SKIPPED = 1
|
||||||
|
ACCEPTED = 2
|
||||||
|
REJECTED = 3
|
||||||
|
|
||||||
|
|
||||||
|
Flag = namedtuple('Flag', ['flag', 'sploit', 'team', 'time', 'status', 'checksystem_response'])
|
||||||
|
SubmitResult = namedtuple('SubmitResult', ['flag', 'status', 'checksystem_response'])
|
||||||
0
server/protocols/__init__.py
Normal file
0
server/protocols/__init__.py
Normal file
83
server/protocols/default.py
Normal file
83
server/protocols/default.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import socket
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
from models import FlagStatus, SubmitResult
|
||||||
|
|
||||||
|
|
||||||
|
RESPONSES = {
|
||||||
|
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
|
||||||
|
'no such flag'],
|
||||||
|
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
|
||||||
|
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
|
||||||
|
'too old', 'not in database', 'already submitted', 'invalid flag',
|
||||||
|
'self', 'invalid', 'already_submitted', 'team_not_found', 'too_old', 'stolen'],
|
||||||
|
}
|
||||||
|
|
||||||
|
READ_TIMEOUT = 5
|
||||||
|
APPEND_TIMEOUT = 0.05
|
||||||
|
BUFSIZE = 4096
|
||||||
|
|
||||||
|
|
||||||
|
def recvall(sock):
|
||||||
|
sock.settimeout(READ_TIMEOUT)
|
||||||
|
chunks = [sock.recv(BUFSIZE)]
|
||||||
|
|
||||||
|
sock.settimeout(APPEND_TIMEOUT)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(BUFSIZE)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
chunks.append(chunk)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
|
||||||
|
sock.settimeout(READ_TIMEOUT)
|
||||||
|
return b''.join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def submit_flags(flags, config):
|
||||||
|
try:
|
||||||
|
sock = socket.create_connection((config['SYSTEM_HOST'], config['SYSTEM_PORT']),
|
||||||
|
READ_TIMEOUT)
|
||||||
|
except (socket.error, ConnectionRefusedError) as e:
|
||||||
|
app.logger.error(f"Failed to connect to checksystem: {e}")
|
||||||
|
# Return all flags as QUEUED since we couldn't submit them
|
||||||
|
for item in flags:
|
||||||
|
yield SubmitResult(item.flag, FlagStatus.QUEUED, "Connection failed")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
greeting = recvall(sock)
|
||||||
|
if b'Welcome' not in greeting:
|
||||||
|
raise Exception('Checksystem does not greet us: {}'.format(greeting))
|
||||||
|
|
||||||
|
sock.sendall(config['TEAM_TOKEN'].encode() + b'\n')
|
||||||
|
invite = recvall(sock)
|
||||||
|
if b'enter your flags' not in invite:
|
||||||
|
raise Exception('Team token seems to be invalid: {}'.format(invite))
|
||||||
|
|
||||||
|
unknown_responses = set()
|
||||||
|
for item in flags:
|
||||||
|
sock.sendall(item.flag.encode() + b'\n')
|
||||||
|
response = recvall(sock).decode().strip()
|
||||||
|
if response:
|
||||||
|
response = response.splitlines()[0]
|
||||||
|
response = response.replace('[{}] '.format(item.flag), '')
|
||||||
|
|
||||||
|
response_lower = response.lower()
|
||||||
|
for status, substrings in RESPONSES.items():
|
||||||
|
if any(s in response_lower for s in substrings):
|
||||||
|
found_status = status
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
found_status = FlagStatus.QUEUED
|
||||||
|
if response not in unknown_responses:
|
||||||
|
unknown_responses.add(response)
|
||||||
|
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
|
||||||
|
|
||||||
|
yield SubmitResult(item.flag, found_status, response)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
76
server/protocols/forcad_tcp.py
Normal file
76
server/protocols/forcad_tcp.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Based on https://gist.github.com/xmikasax/90a0ce5736a4274e46b9958f836951e7
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
from models import FlagStatus, SubmitResult
|
||||||
|
|
||||||
|
|
||||||
|
RESPONSES = {
|
||||||
|
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
|
||||||
|
'no such flag'],
|
||||||
|
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
|
||||||
|
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
|
||||||
|
'too old', 'not in database', 'already submitted', 'invalid flag',
|
||||||
|
'self', 'invalid', 'already_submitted', 'team_not_found', 'too_old', 'stolen'],
|
||||||
|
}
|
||||||
|
|
||||||
|
READ_TIMEOUT = 5
|
||||||
|
APPEND_TIMEOUT = 0.05
|
||||||
|
BUFSIZE = 4096
|
||||||
|
|
||||||
|
|
||||||
|
def recvall(sock):
|
||||||
|
sock.settimeout(READ_TIMEOUT)
|
||||||
|
chunks = [sock.recv(BUFSIZE)]
|
||||||
|
|
||||||
|
sock.settimeout(APPEND_TIMEOUT)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(BUFSIZE)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
chunks.append(chunk)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
|
||||||
|
sock.settimeout(READ_TIMEOUT)
|
||||||
|
return b''.join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def submit_flags(flags, config):
|
||||||
|
sock = socket.create_connection((config['SYSTEM_HOST'], config['SYSTEM_PORT']),
|
||||||
|
READ_TIMEOUT)
|
||||||
|
|
||||||
|
greeting = recvall(sock)
|
||||||
|
if b'Welcome' not in greeting:
|
||||||
|
raise Exception('Checksystem does not greet us: {}'.format(greeting))
|
||||||
|
|
||||||
|
sock.sendall(config['TEAM_TOKEN'].encode() + b'\n')
|
||||||
|
invite = recvall(sock)
|
||||||
|
if b'enter your flags' not in invite:
|
||||||
|
raise Exception('Team token seems to be invalid: {}'.format(invite))
|
||||||
|
|
||||||
|
unknown_responses = set()
|
||||||
|
for item in flags:
|
||||||
|
sock.sendall(item.flag.encode() + b'\n')
|
||||||
|
response = recvall(sock).decode().strip()
|
||||||
|
if response:
|
||||||
|
response = response.splitlines()[0]
|
||||||
|
response = response.replace('[{}] '.format(item.flag), '')
|
||||||
|
|
||||||
|
response_lower = response.lower()
|
||||||
|
for status, substrings in RESPONSES.items():
|
||||||
|
if any(s in response_lower for s in substrings):
|
||||||
|
found_status = status
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
found_status = FlagStatus.QUEUED
|
||||||
|
if response not in unknown_responses:
|
||||||
|
unknown_responses.add(response)
|
||||||
|
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
|
||||||
|
|
||||||
|
yield SubmitResult(item.flag, found_status, response)
|
||||||
|
|
||||||
|
sock.close()
|
||||||
46
server/protocols/ructf_http.py
Normal file
46
server/protocols/ructf_http.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
from models import FlagStatus, SubmitResult
|
||||||
|
|
||||||
|
|
||||||
|
RESPONSES = {
|
||||||
|
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
|
||||||
|
'no such flag'],
|
||||||
|
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
|
||||||
|
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
|
||||||
|
'too old', 'not in database', 'already submitted', 'invalid flag'],
|
||||||
|
}
|
||||||
|
# The RuCTF checksystem adds a signature to all correct flags. It returns
|
||||||
|
# "invalid flag" verdict if the signature is invalid and "no such flag" verdict if
|
||||||
|
# the signature is correct but the flag was not found in the checksystem database.
|
||||||
|
#
|
||||||
|
# The latter situation happens if a checker puts the flag to the service before putting it
|
||||||
|
# to the checksystem database. We should resent the flag later in this case.
|
||||||
|
|
||||||
|
|
||||||
|
TIMEOUT = 5
|
||||||
|
|
||||||
|
|
||||||
|
def submit_flags(flags, config):
|
||||||
|
r = requests.put(config['SYSTEM_URL'],
|
||||||
|
headers={'X-Team-Token': config['SYSTEM_TOKEN']},
|
||||||
|
json=[item.flag for item in flags], timeout=TIMEOUT)
|
||||||
|
|
||||||
|
unknown_responses = set()
|
||||||
|
for item in r.json():
|
||||||
|
response = item['msg'].strip()
|
||||||
|
response = response.replace('[{}] '.format(item['flag']), '')
|
||||||
|
|
||||||
|
response_lower = response.lower()
|
||||||
|
for status, substrings in RESPONSES.items():
|
||||||
|
if any(s in response_lower for s in substrings):
|
||||||
|
found_status = status
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
found_status = FlagStatus.QUEUED
|
||||||
|
if response not in unknown_responses:
|
||||||
|
unknown_responses.add(response)
|
||||||
|
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
|
||||||
|
|
||||||
|
yield SubmitResult(item['flag'], found_status, response)
|
||||||
74
server/protocols/ructf_tcp.py
Normal file
74
server/protocols/ructf_tcp.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import socket
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
from models import FlagStatus, SubmitResult
|
||||||
|
|
||||||
|
|
||||||
|
RESPONSES = {
|
||||||
|
FlagStatus.QUEUED: ['timeout', 'game not started', 'try again later', 'game over', 'is not up',
|
||||||
|
'no such flag'],
|
||||||
|
FlagStatus.ACCEPTED: ['accepted', 'congrat'],
|
||||||
|
FlagStatus.REJECTED: ['bad', 'wrong', 'expired', 'unknown', 'your own',
|
||||||
|
'too old', 'not in database', 'already submitted', 'invalid flag'],
|
||||||
|
}
|
||||||
|
# The RuCTF checksystem adds a signature to all correct flags. It returns
|
||||||
|
# "invalid flag" verdict if the signature is invalid and "no such flag" verdict if
|
||||||
|
# the signature is correct but the flag was not found in the checksystem database.
|
||||||
|
#
|
||||||
|
# The latter situation happens if a checker puts the flag to the service before putting it
|
||||||
|
# to the checksystem database. We should resent the flag later in this case.
|
||||||
|
|
||||||
|
READ_TIMEOUT = 5
|
||||||
|
APPEND_TIMEOUT = 0.05
|
||||||
|
BUFSIZE = 4096
|
||||||
|
|
||||||
|
|
||||||
|
def recvall(sock):
|
||||||
|
sock.settimeout(READ_TIMEOUT)
|
||||||
|
chunks = [sock.recv(BUFSIZE)]
|
||||||
|
|
||||||
|
sock.settimeout(APPEND_TIMEOUT)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
chunk = sock.recv(BUFSIZE)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
chunks.append(chunk)
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
|
||||||
|
sock.settimeout(READ_TIMEOUT)
|
||||||
|
return b''.join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def submit_flags(flags, config):
|
||||||
|
sock = socket.create_connection((config['SYSTEM_HOST'], config['SYSTEM_PORT']),
|
||||||
|
READ_TIMEOUT)
|
||||||
|
|
||||||
|
greeting = recvall(sock)
|
||||||
|
if b'Enter your flags' not in greeting:
|
||||||
|
raise Exception('Checksystem does not greet us: {}'.format(greeting))
|
||||||
|
|
||||||
|
unknown_responses = set()
|
||||||
|
for item in flags:
|
||||||
|
sock.sendall(item.flag.encode() + b'\n')
|
||||||
|
response = recvall(sock).decode().strip()
|
||||||
|
if response:
|
||||||
|
response = response.splitlines()[0]
|
||||||
|
response = response.replace('[{}] '.format(item.flag), '')
|
||||||
|
|
||||||
|
response_lower = response.lower()
|
||||||
|
for status, substrings in RESPONSES.items():
|
||||||
|
if any(s in response_lower for s in substrings):
|
||||||
|
found_status = status
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
found_status = FlagStatus.QUEUED
|
||||||
|
if response not in unknown_responses:
|
||||||
|
unknown_responses.add(response)
|
||||||
|
app.logger.warning('Unknown checksystem response (flag will be resent): %s', response)
|
||||||
|
|
||||||
|
yield SubmitResult(item.flag, found_status, response)
|
||||||
|
|
||||||
|
sock.close()
|
||||||
26
server/protocols/volgactf.py
Normal file
26
server/protocols/volgactf.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from themis.finals.attack.helper import Helper
|
||||||
|
from themis.finals.attack.result import Result
|
||||||
|
|
||||||
|
from models import FlagStatus, SubmitResult
|
||||||
|
|
||||||
|
|
||||||
|
RESPONSES = {
|
||||||
|
FlagStatus.ACCEPTED: [Result.SUCCESS_FLAG_ACCEPTED],
|
||||||
|
FlagStatus.REJECTED: [Result.ERROR_FLAG_EXPIRED, Result.ERROR_FLAG_YOURS,
|
||||||
|
Result.ERROR_FLAG_SUBMITTED, Result.ERROR_FLAG_NOT_FOUND],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def submit_flags(flags, config):
|
||||||
|
h = Helper(config['SYSTEM_HOST'])
|
||||||
|
codes = h.attack(*[item.flag for item in flags])
|
||||||
|
|
||||||
|
for item, code in zip(flags, codes):
|
||||||
|
for status, possible_codes in RESPONSES.items():
|
||||||
|
if code in possible_codes:
|
||||||
|
found_status = status
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
found_status = FlagStatus.QUEUED
|
||||||
|
|
||||||
|
yield SubmitResult(item.flag, found_status, code.name)
|
||||||
49
server/reloader.py
Normal file
49
server/reloader.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
|
||||||
|
|
||||||
|
_config_mtime = None
|
||||||
|
_reload_lock = threading.RLock()
|
||||||
|
|
||||||
|
if "CONFIG" in os.environ:
|
||||||
|
config_path = os.environ["CONFIG"]
|
||||||
|
else:
|
||||||
|
config_path = os.path.join(app.root_path, 'config.py')
|
||||||
|
|
||||||
|
config_spec = importlib.util.spec_from_file_location("server.config", config_path)
|
||||||
|
config_module = importlib.util.module_from_spec(config_spec)
|
||||||
|
config_spec.loader.exec_module(config_module)
|
||||||
|
_cur_config = config_module.CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
"""
|
||||||
|
Returns CONFIG dictionary from config.py module.
|
||||||
|
|
||||||
|
If config.py file was updated since the last call, get_config() reloads
|
||||||
|
the dictionary. If an error happens during reloading, get_config() returns
|
||||||
|
the old dictionary.
|
||||||
|
|
||||||
|
:returns: the newest valid version of the CONFIG dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
global _config_mtime, _cur_config
|
||||||
|
|
||||||
|
cur_mtime = os.stat(config_path).st_mtime_ns
|
||||||
|
if cur_mtime != _config_mtime:
|
||||||
|
with _reload_lock:
|
||||||
|
if cur_mtime != _config_mtime:
|
||||||
|
try:
|
||||||
|
config_spec.loader.exec_module(config_module)
|
||||||
|
_cur_config = config_module.CONFIG
|
||||||
|
app.logger.info('New config loaded')
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error('Failed to reload config: %s', e)
|
||||||
|
|
||||||
|
_config_mtime = cur_mtime
|
||||||
|
|
||||||
|
return _cur_config
|
||||||
14
server/requirements.txt
Normal file
14
server/requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
certifi>=2018.1.18
|
||||||
|
chardet>=3.0.4
|
||||||
|
click>=6.7
|
||||||
|
Flask>=1.1.1
|
||||||
|
idna>=2.6
|
||||||
|
itsdangerous>=0.24
|
||||||
|
Jinja2>=2.11.3
|
||||||
|
MarkupSafe>=1.1.1
|
||||||
|
requests>=2.22.0
|
||||||
|
themis.finals.attack.helper>=1.1.0
|
||||||
|
themis.finals.attack.result>=1.3.0
|
||||||
|
urllib3>=1.26.5
|
||||||
|
Werkzeug>=0.16.0
|
||||||
|
psycopg2-binary
|
||||||
13
server/schema.sql
Normal file
13
server/schema.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS flags (
|
||||||
|
flag TEXT PRIMARY KEY,
|
||||||
|
sploit TEXT,
|
||||||
|
team TEXT,
|
||||||
|
time INTEGER,
|
||||||
|
status TEXT,
|
||||||
|
checksystem_response TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS flags_sploit ON flags(sploit);
|
||||||
|
CREATE INDEX IF NOT EXISTS flags_team ON flags(team);
|
||||||
|
CREATE INDEX IF NOT EXISTS flags_status_time ON flags(status, time);
|
||||||
|
CREATE INDEX IF NOT EXISTS flags_time ON flags(time);
|
||||||
37
server/spam.py
Normal file
37
server/spam.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
def generate_spam_flag():
|
||||||
|
import base64, hashlib, os, re
|
||||||
|
encode = lambda s: re.sub(r'[a-z/+=\n]', r'', base64.encodebytes(s).decode()).upper()
|
||||||
|
secret = '1234'
|
||||||
|
|
||||||
|
prefix = encode(os.urandom(64))[:16]
|
||||||
|
suffix = encode(hashlib.sha256((prefix + secret).encode()).digest())[:15]
|
||||||
|
return prefix + suffix + '='
|
||||||
|
|
||||||
|
|
||||||
|
def is_spam_flag(flag):
|
||||||
|
import base64, hashlib, re
|
||||||
|
encode = lambda s: re.sub(r'[a-z/+=\n]', r'', base64.encodebytes(s).decode()).upper()
|
||||||
|
secret = '1234'
|
||||||
|
|
||||||
|
prefix = flag[:16].upper()
|
||||||
|
suffix = encode(hashlib.sha256((prefix + secret).encode()).digest())[:15]
|
||||||
|
return flag[16:].upper() == suffix + '='
|
||||||
|
|
||||||
|
|
||||||
|
def test():
|
||||||
|
import base64, hashlib, os, re
|
||||||
|
encode = lambda s: re.sub(r'[a-z/+=\n]', r'', base64.encodebytes(s).decode()).upper()
|
||||||
|
for i in range(10**4):
|
||||||
|
flag = encode(os.urandom(128))[:31] + '='
|
||||||
|
if i < 30:
|
||||||
|
print(flag)
|
||||||
|
assert not is_spam_flag(flag)
|
||||||
|
|
||||||
|
for i in range(10**3):
|
||||||
|
assert is_spam_flag(generate_spam_flag())
|
||||||
|
|
||||||
|
print('Ok')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test()
|
||||||
12
server/standalone.py
Normal file
12
server/standalone.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import threading
|
||||||
|
import werkzeug.serving
|
||||||
|
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
import submit_loop
|
||||||
|
|
||||||
|
|
||||||
|
if not werkzeug.serving.is_running_from_reloader():
|
||||||
|
submit_thread = threading.Thread(target=submit_loop.run_loop)
|
||||||
|
submit_thread.start()
|
||||||
|
|
||||||
12
server/start_docker.sh
Executable file
12
server/start_docker.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Get port from config.py
|
||||||
|
PORT=$(python3 -c "from config import CONFIG; print(CONFIG.get('SYSTEM_PORT'))")
|
||||||
|
WEB_PORT=$(python3 -c "from config import CONFIG; print(CONFIG.get('PORT'))")
|
||||||
|
|
||||||
|
# Build and run docker with port forwarding
|
||||||
|
if [ "$1" = "--build" ]; then
|
||||||
|
docker build -t pb_farm .
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run --rm -p $PORT:$PORT pb_farm
|
||||||
6
server/start_server.sh
Executable file
6
server/start_server.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Use FLASK_DEBUG=True if needed
|
||||||
|
|
||||||
|
PORT=$(python3 -c "from config import CONFIG; print(CONFIG.get('PORT'))")
|
||||||
|
FLASK_APP=$(dirname $(readlink -f $0))/standalone.py python3 -m flask run --host 0.0.0.0 --with-threads --port $PORT
|
||||||
9551
server/static/css/bootswatch-flatly.css
Normal file
9551
server/static/css/bootswatch-flatly.css
Normal file
File diff suppressed because it is too large
Load Diff
26
server/static/css/style.css
Normal file
26
server/static/css/style.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 300;
|
||||||
|
padding: 15px 15px;
|
||||||
|
color: #e52250;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text-with-flags {
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
BIN
server/static/img/logo.png
Normal file
BIN
server/static/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
5
server/static/js/jquery.min.js
vendored
Normal file
5
server/static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
144
server/static/js/ui.js
Normal file
144
server/static/js/ui.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
function padLeft(s, length) {
|
||||||
|
s = s.toString();
|
||||||
|
while (s.length < length)
|
||||||
|
s = '0' + s;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateToString(date) {
|
||||||
|
return padLeft(date.getFullYear(), 4) + '-' + padLeft(date.getMonth() + 1, 2) + '-' +
|
||||||
|
padLeft(date.getDate(), 2) + ' ' +
|
||||||
|
padLeft(date.getHours(), 2) + ':' + padLeft(date.getMinutes(), 2) + ':' +
|
||||||
|
padLeft(date.getSeconds(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return $('<div>').text(text).html();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateFlagTableRows(rows) {
|
||||||
|
var html = '';
|
||||||
|
rows.forEach(function (item) {
|
||||||
|
var cells = [
|
||||||
|
item.sploit,
|
||||||
|
item.team !== null ? item.team : '',
|
||||||
|
item.flag,
|
||||||
|
dateToString(new Date(item.time * 1000)),
|
||||||
|
item.status,
|
||||||
|
item.checksystem_response !== null ? item.checksystem_response : ''
|
||||||
|
];
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
cells.forEach(function (text) {
|
||||||
|
html += '<td>' + escapeHtml(text) + '</td>';
|
||||||
|
});
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePaginator(totalCount, rowsPerPage, pageNumber) {
|
||||||
|
var totalPages = Math.ceil(totalCount / rowsPerPage);
|
||||||
|
var firstShown = Math.max(1, pageNumber - 3);
|
||||||
|
var lastShown = Math.min(totalPages, pageNumber + 3);
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
if (firstShown > 1)
|
||||||
|
html += '<li class="page-item"><a class="page-link" href="#" data-content="1">«</a></li>';
|
||||||
|
|
||||||
|
for (var i = firstShown; i <= lastShown; i++) {
|
||||||
|
var extraClasses = (i === pageNumber ? "active" : "");
|
||||||
|
html += '<li class="page-item ' + extraClasses + '">' +
|
||||||
|
'<a class="page-link" href="#" data-content="' + i + '">' + i + '</a>' +
|
||||||
|
'</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastShown < totalPages)
|
||||||
|
html += '<li class="page-item">' +
|
||||||
|
'<a class="page-link" href="#" data-content="' + totalPages + '">»</a>' +
|
||||||
|
'</li>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPageNumber() {
|
||||||
|
return parseInt($('#page-number').val());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageNumber(number) {
|
||||||
|
$('#page-number').val(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryInProgress = false;
|
||||||
|
|
||||||
|
function showFlags() {
|
||||||
|
if (queryInProgress)
|
||||||
|
return;
|
||||||
|
queryInProgress = true;
|
||||||
|
|
||||||
|
$('.search-results').hide();
|
||||||
|
$('.query-status').html('Loading...').show();
|
||||||
|
|
||||||
|
$.post('/ui/show_flags', $('#show-flags-form').serialize())
|
||||||
|
.done(function (response) {
|
||||||
|
$('.search-results tbody').html(generateFlagTableRows(response.rows));
|
||||||
|
|
||||||
|
$('.search-results .total-count').text(response.total_count);
|
||||||
|
$('.search-results .pagination').html(generatePaginator(
|
||||||
|
response.total_count, response.rows_per_page, getPageNumber()));
|
||||||
|
$('.search-results .page-link').click(function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setPageNumber($(this).data("content"));
|
||||||
|
showFlags();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.query-status').hide();
|
||||||
|
$('.search-results').show();
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
$('.query-status').html("Failed to load flags from the farm server");
|
||||||
|
})
|
||||||
|
.always(function () {
|
||||||
|
queryInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postFlagsManual() {
|
||||||
|
if (queryInProgress)
|
||||||
|
return;
|
||||||
|
queryInProgress = true;
|
||||||
|
|
||||||
|
$.post('/ui/post_flags_manual', $('#post-flags-manual-form').serialize())
|
||||||
|
.done(function () {
|
||||||
|
var sploitSelect = $('#sploit-select');
|
||||||
|
if ($('#sploit-manual-option').empty())
|
||||||
|
sploitSelect.append($('<option id="sploit-manual-option">Manual</option>'));
|
||||||
|
sploitSelect.val('Manual');
|
||||||
|
|
||||||
|
$('#team-select, #flag-input, #time-since-input, #time-until-input, ' +
|
||||||
|
'#status-select, #checksystem-response-input').val('');
|
||||||
|
|
||||||
|
queryInProgress = false;
|
||||||
|
showFlags();
|
||||||
|
})
|
||||||
|
.fail(function () {
|
||||||
|
$('.query-status').html("Failed to post flags to the farm server");
|
||||||
|
queryInProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
showFlags();
|
||||||
|
|
||||||
|
$('#show-flags-form').submit(function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setPageNumber(1);
|
||||||
|
showFlags();
|
||||||
|
});
|
||||||
|
$('#post-flags-manual-form').submit(function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
postFlagsManual();
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
server/static/logo.png
Normal file
BIN
server/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
95
server/submit_loop.py
Executable file
95
server/submit_loop.py
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
import database
|
||||||
|
import reloader
|
||||||
|
from models import Flag, FlagStatus, SubmitResult
|
||||||
|
|
||||||
|
|
||||||
|
def get_fair_share(groups, limit):
|
||||||
|
if not groups:
|
||||||
|
return []
|
||||||
|
|
||||||
|
groups = sorted(groups, key=len)
|
||||||
|
places_left = limit
|
||||||
|
group_count = len(groups)
|
||||||
|
fair_share = places_left // group_count
|
||||||
|
|
||||||
|
result = []
|
||||||
|
residuals = []
|
||||||
|
for group in groups:
|
||||||
|
if len(group) <= fair_share:
|
||||||
|
result += group
|
||||||
|
|
||||||
|
places_left -= len(group)
|
||||||
|
group_count -= 1
|
||||||
|
if group_count > 0:
|
||||||
|
fair_share = places_left // group_count
|
||||||
|
# The fair share could have increased because the processed group
|
||||||
|
# had a few elements. Sorting order guarantees that the smaller
|
||||||
|
# groups will be processed first.
|
||||||
|
else:
|
||||||
|
selected = random.sample(group, fair_share + 1)
|
||||||
|
result += selected[:-1]
|
||||||
|
residuals.append(selected[-1])
|
||||||
|
result += random.sample(residuals, min(limit - len(result), len(residuals)))
|
||||||
|
|
||||||
|
random.shuffle(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def submit_flags(flags, config):
|
||||||
|
module = importlib.import_module('protocols.' + config['SYSTEM_PROTOCOL'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
return list(module.submit_flags(flags, config))
|
||||||
|
except Exception as e:
|
||||||
|
message = '{}: {}'.format(type(e).__name__, str(e))
|
||||||
|
app.logger.exception('Exception on submitting flags')
|
||||||
|
return [SubmitResult(item.flag, FlagStatus.QUEUED, message) for item in flags]
|
||||||
|
|
||||||
|
|
||||||
|
def run_loop():
|
||||||
|
app.logger.info('Starting submit loop')
|
||||||
|
with app.app_context():
|
||||||
|
db = database.get(context_bound=False)
|
||||||
|
cursor = db.cursor()
|
||||||
|
while True:
|
||||||
|
submit_start_time = time.time()
|
||||||
|
|
||||||
|
config = reloader.get_config()
|
||||||
|
|
||||||
|
skip_time = round(submit_start_time - config['FLAG_LIFETIME'])
|
||||||
|
cursor.execute("UPDATE flags SET status = %s WHERE status = %s AND time < %s",
|
||||||
|
(FlagStatus.SKIPPED.name, FlagStatus.QUEUED.name, skip_time))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM flags WHERE status = %s", (FlagStatus.QUEUED.name,))
|
||||||
|
queued_flags = [Flag(**item) for item in cursor.fetchall()]
|
||||||
|
|
||||||
|
if queued_flags:
|
||||||
|
grouped_flags = defaultdict(list)
|
||||||
|
for item in queued_flags:
|
||||||
|
grouped_flags[item.sploit, item.team].append(item)
|
||||||
|
flags = get_fair_share(grouped_flags.values(), config['SUBMIT_FLAG_LIMIT'])
|
||||||
|
|
||||||
|
app.logger.debug('Submitting %s flags (out of %s in queue)', len(flags), len(queued_flags))
|
||||||
|
results = submit_flags(flags, config)
|
||||||
|
|
||||||
|
rows = [(item.status.name, item.checksystem_response, item.flag) for item in results]
|
||||||
|
cursor.executemany("UPDATE flags SET status = %s, checksystem_response = %s"
|
||||||
|
"WHERE flag = %s", rows)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
submit_spent = time.time() - submit_start_time
|
||||||
|
if config['SUBMIT_PERIOD'] > submit_spent:
|
||||||
|
time.sleep(config['SUBMIT_PERIOD'] - submit_spent)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_loop()
|
||||||
48
server/templates/hello.html
Normal file
48
server/templates/hello.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login Required</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.login-form {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type="submit"]:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-form">
|
||||||
|
<h2>Добро пожаловать!</h2>
|
||||||
|
<form method="POST">
|
||||||
|
<label for="password">Пароль:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
<input type="submit" value="Войти">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
150
server/templates/index.html
Normal file
150
server/templates/index.html
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
<link rel="icon" href="/static/logo.png">
|
||||||
|
|
||||||
|
<title>Poison Berries Farm - Server</title>
|
||||||
|
|
||||||
|
<link href="/static/css/bootswatch-flatly.css" rel="stylesheet">
|
||||||
|
<link href="/static/css/style.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark py-0">
|
||||||
|
<img class="logo" src="/static/img/logo.png">
|
||||||
|
<div class="navbar-brand">Poison Berries Farm</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card border-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Show Flags</h4>
|
||||||
|
<form id="show-flags-form">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="sploit-select">Sploit</label>
|
||||||
|
<select class="form-control form-control-sm" id="sploit-select" name="sploit">
|
||||||
|
<option value="">All</option>
|
||||||
|
{% for item in distinct_values['sploit'] %}
|
||||||
|
<option>{{ item }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="team-select">Team</label>
|
||||||
|
<select class="form-control form-control-sm" id="team-select" name="team">
|
||||||
|
<option value="">All</option>
|
||||||
|
{% for item in distinct_values['team'] %}
|
||||||
|
<option>{{ item }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="flag-input">
|
||||||
|
Flag
|
||||||
|
<small class="text-muted ml-2">substring, case-insensitive</small>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="flag-input" name="flag">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="time-since-input">
|
||||||
|
Since
|
||||||
|
<small class="text-muted ml-2">{{ server_tz_name }}</small>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="time-since-input"
|
||||||
|
name="time-since"
|
||||||
|
placeholder="yyyy-mm-dd hh:mm">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="time-until-input">
|
||||||
|
Until
|
||||||
|
<small class="text-muted ml-2">{{ server_tz_name }}</small>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="time-until-input"
|
||||||
|
name="time-until"
|
||||||
|
placeholder="yyyy-mm-dd hh:mm">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="status-select">Status</label>
|
||||||
|
<select class="form-control form-control-sm" id="status-select" name="status">
|
||||||
|
<option value="">All</option>
|
||||||
|
{% for item in distinct_values['status'] %}
|
||||||
|
<option>{{ item }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="checksystem-response-input">
|
||||||
|
Checksystem response
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" id="checksystem-response-input"
|
||||||
|
name="checksystem_response">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm submit-btn">Show</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" value="1" id="page-number" name="page-number">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card border-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Add Flags Manually</h4>
|
||||||
|
<form id="post-flags-manual-form">
|
||||||
|
<label for="text-with-flags">
|
||||||
|
Text with flags
|
||||||
|
<small class="text-muted ml-2">flag format: {{ flag_format }}</small>
|
||||||
|
</label>
|
||||||
|
<textarea class="form-control form-control-sm mb-3" id="text-with-flags" name="text"></textarea>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 text-center query-status"></div>
|
||||||
|
|
||||||
|
<div class="search-results" style="display: none;">
|
||||||
|
<p>Found <span class="total-count"></span> flags</p>
|
||||||
|
|
||||||
|
<ul class="pagination pagination-sm"></ul>
|
||||||
|
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-secondary">
|
||||||
|
<th scope="col">Sploit</th>
|
||||||
|
<th scope="col">Team</th>
|
||||||
|
<th scope="col">Flag</th>
|
||||||
|
<th scope="col">Time</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Checksystem Response</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<ul class="pagination pagination-sm"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
|
<script src="/static/js/ui.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
119
server/views.py
Normal file
119
server/views.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import re
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from flask import jsonify, render_template, request, redirect
|
||||||
|
|
||||||
|
from __init__ import app
|
||||||
|
import auth
|
||||||
|
import database
|
||||||
|
import reloader
|
||||||
|
from models import FlagStatus
|
||||||
|
|
||||||
|
|
||||||
|
@app.template_filter('timestamp_to_datetime')
|
||||||
|
def timestamp_to_datetime(s):
|
||||||
|
return datetime.fromtimestamp(s)
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def index_redirect():
|
||||||
|
if request.method == 'POST':
|
||||||
|
response = redirect('/')
|
||||||
|
response.set_cookie('password', request.form['password'])
|
||||||
|
return response
|
||||||
|
config = reloader.get_config()
|
||||||
|
if request.cookies.get('password') == config['SERVER_PASSWORD']:
|
||||||
|
return redirect('/farm')
|
||||||
|
return render_template('hello.html')
|
||||||
|
|
||||||
|
@app.route('/farm')
|
||||||
|
@auth.auth_required
|
||||||
|
def index():
|
||||||
|
distinct_values = {}
|
||||||
|
for column in ['sploit', 'status', 'team']:
|
||||||
|
rows = database.query('SELECT DISTINCT {} FROM flags ORDER BY {}'.format(column, column))
|
||||||
|
distinct_values[column] = [item[column] for item in rows] # Access by key, not index
|
||||||
|
|
||||||
|
config = reloader.get_config()
|
||||||
|
|
||||||
|
server_tz_name = time.strftime('%Z')
|
||||||
|
if server_tz_name.startswith('+'):
|
||||||
|
server_tz_name = 'UTC' + server_tz_name
|
||||||
|
|
||||||
|
return render_template('index.html',
|
||||||
|
flag_format=config['FLAG_FORMAT'],
|
||||||
|
distinct_values=distinct_values,
|
||||||
|
server_tz_name=server_tz_name)
|
||||||
|
|
||||||
|
|
||||||
|
FORM_DATETIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||||
|
FLAGS_PER_PAGE = 30
|
||||||
|
|
||||||
|
@app.route('/ui/show_flags', methods=['POST'])
|
||||||
|
@auth.auth_required
|
||||||
|
def show_flags():
|
||||||
|
conditions = []
|
||||||
|
for column in ['sploit', 'status', 'team']:
|
||||||
|
value = request.form[column]
|
||||||
|
if value:
|
||||||
|
conditions.append(('{} = %s'.format(column), value))
|
||||||
|
for column in ['flag', 'checksystem_response']:
|
||||||
|
value = request.form[column]
|
||||||
|
if value:
|
||||||
|
conditions.append(('LOWER({}) LIKE %s'.format(column), '%' + value.lower() + '%')) # Changed INSTR to LIKE
|
||||||
|
for param in ['time-since', 'time-until']:
|
||||||
|
value = request.form[param].strip()
|
||||||
|
if value:
|
||||||
|
timestamp = round(datetime.strptime(value, FORM_DATETIME_FORMAT).timestamp())
|
||||||
|
sign = '>=' if param == 'time-since' else '<='
|
||||||
|
conditions.append(('time {} %s'.format(sign), timestamp))
|
||||||
|
page_number = int(request.form['page-number'])
|
||||||
|
if page_number < 1:
|
||||||
|
raise ValueError('Invalid page-number')
|
||||||
|
|
||||||
|
if conditions:
|
||||||
|
chunks, values = list(zip(*conditions))
|
||||||
|
conditions_sql = 'WHERE ' + ' AND '.join(chunks)
|
||||||
|
conditions_args = list(values)
|
||||||
|
else:
|
||||||
|
conditions_sql = ''
|
||||||
|
conditions_args = []
|
||||||
|
|
||||||
|
sql = 'SELECT * FROM flags ' + conditions_sql + ' ORDER BY time DESC LIMIT %s OFFSET %s'
|
||||||
|
args = conditions_args + [FLAGS_PER_PAGE, FLAGS_PER_PAGE * (page_number - 1)]
|
||||||
|
flags = database.query(sql, args)
|
||||||
|
|
||||||
|
sql = 'SELECT COUNT(*) as count FROM flags ' + conditions_sql # Added alias
|
||||||
|
args = conditions_args
|
||||||
|
total_count_result = database.query(sql, args)
|
||||||
|
total_count = total_count_result[0]['count'] if total_count_result else 0 # Access by key
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'rows': [dict(item) for item in flags],
|
||||||
|
'rows_per_page': FLAGS_PER_PAGE,
|
||||||
|
'total_count': total_count,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/ui/post_flags_manual', methods=['POST'])
|
||||||
|
@auth.auth_required
|
||||||
|
def post_flags_manual():
|
||||||
|
config = reloader.get_config()
|
||||||
|
flags = re.findall(config['FLAG_FORMAT'], request.form['text'])
|
||||||
|
|
||||||
|
cur_time = round(time.time())
|
||||||
|
rows = [(item, 'Manual', '*', cur_time, FlagStatus.QUEUED.name)
|
||||||
|
for item in flags]
|
||||||
|
app.logger.info('Inserting flags: %s', rows)
|
||||||
|
|
||||||
|
# Use the execute function instead of direct db access
|
||||||
|
for row in rows:
|
||||||
|
database.execute(
|
||||||
|
"INSERT INTO flags (flag, sploit, team, time, status) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (flag) DO NOTHING",
|
||||||
|
row
|
||||||
|
)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@app.route('/robots.txt', methods=['GET'])
|
||||||
|
def robots_txt():
|
||||||
|
return 'User-agent: *\nDisallow: /'
|
||||||
Reference in New Issue
Block a user