- Rename repo/domain: attack-defense-platform / attackdefense.imrnes.team (all refs replaced)
- challenge_registry.json: single source of truth (28 challs across gemastik18/xvi/xvii)
- teams.py: registry-driven CHALLENGES, set_challenge_enabled, sync_challenge_runtime
(apply enable/disable to live teams: build/up or stop/remove + receiver restart)
- compose_gen.py: render per-team compose from canonical per-challenge templates
(image reuse, per-team ports 30xxx, flag mounts, passwords)
- gen_canonical_composes.py: canonical docker-compose.yml for all services
- import_new_challenges.py: import XVI/XVII services + EOL base image fixes
(debian:buster→bookworm, node:14→20, python:3.7-slim→3.11)
- receiver: xvi package (10 checkers) + xvii package (12 generic checkers),
Challenge base reads PASSWORD_<team_port> from env; gen_receiver_main.py
generates per-team main.py from registry
- main.py: /api/challenges returns full registry; PATCH /api/challenges/<name>
toggles enabled + applies to live teams
- index.html: 🏗️ Challenge Manager tab (toggle per challenge, grouped by set)
- SLA bonus now dynamic (all enabled challenges, not hardcoded 6)
251 lines
9.4 KiB
Python
251 lines
9.4 KiB
Python
import tornado.ioloop
|
|
import tornado.web
|
|
import tornado.websocket
|
|
import tornado.gen
|
|
import threading
|
|
import requests
|
|
import json
|
|
import asyncio
|
|
import ipaddress
|
|
import socket
|
|
import random
|
|
import uuid
|
|
import os
|
|
from urllib.parse import urlparse
|
|
import urllib3
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
# Dictionary to keep track of WebSocket clients
|
|
clients = {}
|
|
|
|
# Expanded templates for more diverse scan results
|
|
vulnerability_templates = [
|
|
{'template': 'CVE-2021-26855', 'severity': 'critical', 'description': 'Exchange Server SSRF vulnerability detected.'},
|
|
{'template': 'CVE-2021-34473', 'severity': 'high', 'description': 'ProxyShell vulnerability detected.'},
|
|
{'template': 'CVE-2021-34527', 'severity': 'critical', 'description': 'PrintNightmare vulnerability detected.'},
|
|
{'template': 'CVE-2020-1472', 'severity': 'critical', 'description': 'Zerologon vulnerability detected.'},
|
|
{'template': 'CVE-2019-19781', 'severity': 'medium', 'description': 'Citrix ADC Remote Code Execution detected.'},
|
|
{'template': 'CVE-2020-0601', 'severity': 'high', 'description': 'CryptoAPI Spoofing vulnerability detected.'},
|
|
{'template': 'CVE-2021-21972', 'severity': 'high', 'description': 'VMware vSphere Client RCE detected.'},
|
|
{'template': 'CVE-2017-11882', 'severity': 'medium', 'description': 'Microsoft Office Memory Corruption detected.'}
|
|
]
|
|
|
|
# Let's simulate a scan. We are not doing an actual scan on this PoC to prevent unnecessary problem in the network.
|
|
# For actual scanning it should be straight forward. We just need to run all the tools with subprocess, catch the results and send them to the client.
|
|
async def simulate_scan(target, ws_handler, scan_id):
|
|
ports = random.sample([22, 80, 443, 8080, 3306, 5432], random.randint(2, 4))
|
|
dummy_progress = [
|
|
"Initializing scan engine...",
|
|
"Checking for internet connectivity...",
|
|
"Loading vulnerability templates...",
|
|
f"Starting scan with scan_id {scan_id}...",
|
|
f"Target acquired: {target}",
|
|
f"Scanning target IP: {random.randint(100, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}.{random.randint(0, 255)}",
|
|
f"Running OS fingerprinting...",
|
|
f"Port scan started on {target}...",
|
|
f"Detected services on ports: {ports}",
|
|
"Loading 15 vulnerability templates for scanning...",
|
|
"[info] Running template: CVE-2021-26855 (Exchange Server SSRF)",
|
|
"[info] Running template: CVE-2021-34473 (ProxyShell)",
|
|
"[info] Running template: CVE-2021-34527 (PrintNightmare)",
|
|
"[info] Running template: CVE-2020-1472 (Zerologon)",
|
|
"[info] Running template: CVE-2019-19781 (Citrix ADC Remote Code Execution)",
|
|
"[info] Running template: CVE-2020-0601 (CryptoAPI Spoofing)",
|
|
"[info] Running template: CVE-2021-21972 (VMware vSphere RCE)",
|
|
"[info] Running template: CVE-2017-11882 (Office Memory Corruption)",
|
|
"Analyzing detected vulnerabilities...",
|
|
"Compiling result..."
|
|
]
|
|
|
|
for progress in dummy_progress:
|
|
await asyncio.sleep(0.5)
|
|
try:
|
|
ws_handler.write_message(json.dumps({"event": "scan_progress", 'data': progress}))
|
|
except Exception:
|
|
print("WebSocket connection closed, cannot send progress.", flush=True)
|
|
|
|
# Emit the final scan results
|
|
try:
|
|
ws_handler.write_message(json.dumps({"event": "scan_result", 'data': f"Result saved to report/{scan_id}.json. Please contact admin for detailed report"}))
|
|
except Exception:
|
|
print("WebSocket connection closed, cannot send final result.", flush=True)
|
|
|
|
# Final results
|
|
dummy_results = random.sample(vulnerability_templates, random.randint(2, 5))
|
|
|
|
with open(f"report/{scan_id}.json", "w") as f:
|
|
json.dump(
|
|
{
|
|
"open_ports": [str(i) for i in ports],
|
|
"vulnerabilities": dummy_results,
|
|
"scan_status": "completed",
|
|
"scan_duration": f"{random.randint(1,10)} minutes"
|
|
}, f)
|
|
|
|
def start_scan_wrapper(target, ws_handler, scan_id):
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
loop.run_until_complete(simulate_scan(target, ws_handler, scan_id))
|
|
loop.close()
|
|
|
|
def get_local_ips():
|
|
"""Get a list of all local IP addresses of the server."""
|
|
local_ips = []
|
|
hostname = socket.gethostname()
|
|
for ip in socket.gethostbyname_ex(hostname)[2]:
|
|
local_ips.append(ipaddress.ip_address(ip))
|
|
return local_ips
|
|
|
|
def is_local_ip(ip, local_ips):
|
|
"""Check if the IP address is one of the local IP addresses of the server."""
|
|
try:
|
|
ip_obj = ipaddress.ip_address(ip)
|
|
return ip_obj in local_ips or ip_obj.is_loopback
|
|
except ValueError:
|
|
return False
|
|
|
|
def validate_url(url):
|
|
parsed_url = urlparse(url)
|
|
if parsed_url.scheme != "http" and parsed_url.scheme != "https":
|
|
return False
|
|
|
|
local_ips = get_local_ips()
|
|
netloc = parsed_url.netloc.split(":")[0]
|
|
|
|
# domain check
|
|
if not netloc.replace(".", "").isdigit():
|
|
# resolve the domain to ip
|
|
try:
|
|
ip = socket.gethostbyname(netloc)
|
|
except socket.gaierror:
|
|
return False
|
|
|
|
if is_local_ip(ip, local_ips):
|
|
return False
|
|
else:
|
|
# check if the ip is local
|
|
if is_local_ip(netloc, local_ips):
|
|
return False
|
|
return True
|
|
|
|
# WebSocket handler
|
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
|
def check_origin(self, origin):
|
|
return True
|
|
|
|
def open(self):
|
|
self.cid = self.get_argument("client_id")
|
|
print("WebSocket opened", flush=True)
|
|
clients[self.cid] = self
|
|
|
|
def on_message(self, message):
|
|
print(f"Received message: {message}")
|
|
|
|
def on_close(self):
|
|
print("WebSocket closed", flush=True)
|
|
if self.cid in clients:
|
|
del clients[self.cid]
|
|
|
|
# Start scan endpoint
|
|
class StartScanHandler(tornado.web.RequestHandler):
|
|
def post(self):
|
|
data = json.loads(self.request.body)
|
|
target = data.get('target')
|
|
if not validate_url(target):
|
|
self.set_status(400)
|
|
self.write(json.dumps({'error': 'Invalid Target'}))
|
|
return
|
|
cid = data.get('client_id')
|
|
scan_id = str(uuid.uuid4())
|
|
|
|
if not target:
|
|
self.set_status(400)
|
|
self.finish(json.dumps({'error': 'No target provided'}))
|
|
elif cid not in clients:
|
|
self.set_status(400)
|
|
self.finish(json.dumps({'error': 'Invalid client ID'}))
|
|
else:
|
|
# Simulate the scan in a background thread
|
|
threading.Thread(target=start_scan_wrapper, args=(target, clients[cid], scan_id)).start()
|
|
self.write(json.dumps({'status': 'scanning started', 'scan_id': scan_id}))
|
|
|
|
# Test website handler (GET/POST)
|
|
class TestWebsiteHandler(tornado.web.RequestHandler):
|
|
def post(self):
|
|
data = json.loads(self.request.body)
|
|
url = data.get('url')
|
|
if not url:
|
|
self.set_status(400)
|
|
self.write(json.dumps({'error': 'No URL provided'}))
|
|
return
|
|
|
|
self.check_url_alive(url)
|
|
|
|
def check_url_alive(self, url):
|
|
try:
|
|
if not validate_url(url):
|
|
self.set_status(400)
|
|
self.write(json.dumps({'error': 'Invalid Target'}))
|
|
return
|
|
|
|
# cleanup url
|
|
parsed_url = urlparse(url)
|
|
cleaned_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
|
|
# Check if the URL is alive
|
|
response = requests.get(cleaned_url, allow_redirects=False, verify=False, timeout=10)
|
|
self.set_status(response.status_code)
|
|
self.finish()
|
|
except requests.RequestException:
|
|
self.set_status(500)
|
|
self.finish()
|
|
|
|
# Generate Report Endpoint (reads from the saved JSON report file)
|
|
class GenerateReportHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
scan_id = self.get_argument("scan_id", None)
|
|
report_name = self.get_argument("report_name", "")
|
|
|
|
if not scan_id:
|
|
self.set_status(400)
|
|
self.write(json.dumps({"error": "Missing scan_id"}))
|
|
return
|
|
|
|
# File path for the report
|
|
report_file = f"report/{scan_id}.json"
|
|
|
|
# Check if the report file exists
|
|
if not os.path.exists(report_file):
|
|
self.set_status(404)
|
|
self.write(json.dumps({"error": f"Report for scan_id {scan_id} not found"}))
|
|
return
|
|
|
|
# Load the report data from the JSON file
|
|
with open(report_file, "r") as f:
|
|
scan_data = json.load(f)
|
|
|
|
# Read the HTML template from the file
|
|
with open("report_template.html", "r") as template:
|
|
report_template = template.read()
|
|
|
|
report_template = report_template.replace("<REPORT_NAME>", report_name)
|
|
|
|
# Dynamically render the report with scan data
|
|
self.write(tornado.template.Template(report_template).generate(
|
|
scan_id=scan_id,
|
|
scan_data=scan_data
|
|
))
|
|
|
|
def make_app():
|
|
return tornado.web.Application([
|
|
(r"/api/public/ws/", WebSocketHandler),
|
|
(r"/api/public/startscan", StartScanHandler),
|
|
(r"/api/public/test", TestWebsiteHandler),
|
|
(r"/api/private/report", GenerateReportHandler)
|
|
])
|
|
|
|
if __name__ == "__main__":
|
|
app = make_app()
|
|
app.listen(5000)
|
|
tornado.ioloop.IOLoop.instance().start() |