-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c449a5d
Showing
52 changed files
with
1,796 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.ctf/ | ||
__pycache__ | ||
node_modules | ||
.vscode |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
# script.py | ||
DEBUG = True | ||
|
||
# GCP project | ||
INSTANCE_NAME = "mp-haproxy-instance" | ||
INSTANCE_ZONE = "europe-west1-b" | ||
HAPROXY_USER = "root" | ||
PROJECT_ID = "mentoring-program-371116" | ||
|
||
# HAProxy config | ||
STATS_PORT = 8080 | ||
STATS_USER = "shellmates" | ||
STATS_PASSWORD = "4YALhtxXP4qAsqPNxRinok0UeXDu6H" | ||
DOMAIN_NAME = "" | ||
HAPROXY_ROOT = "/etc/haproxy" | ||
HAPROXY_MAPS_ROOT = f"{HAPROXY_ROOT}/maps" | ||
HTTP_HOSTS_MAP = "http-hosts.map" | ||
SNI_MAP = "sni.map" | ||
HTTP_HOSTS_MAP_PATH = f"{HAPROXY_MAPS_ROOT}/{HTTP_HOSTS_MAP}" | ||
SNI_MAP_PATH = f"{HAPROXY_MAPS_ROOT}/{SNI_MAP}" | ||
SSL_CERTIFICATE_PATH = f"/etc/haproxy/{DOMAIN_NAME}.pem" | ||
NODES = [ | ||
{ | ||
"name": "node1", | ||
"ip": "10.132.0.5" | ||
}, | ||
] | ||
HTTP_TYPE = "http" | ||
TCP_TYPE = "tcp" | ||
CHALLENGES_JSON_PATH = "config/chals.json" | ||
TEMPLATES_DIR = "templates" | ||
HAPROXY_CFG = "haproxy.cfg" | ||
HAPROXY_CONFIG_DIR = "config/haproxy" | ||
DEPLOY = True | ||
|
||
# Helm config | ||
USE_REMOTE_REPO = False | ||
HELM_REGISTRY = "" | ||
HELM_CHART_REPO = "helm-charts/ctf-challenge-chart" | ||
HELM_CHART_VERSION = "0.1.0" | ||
|
||
# IP blacklisting | ||
IP_BAN_MINUTES = 2 # 2 minutes | ||
CONN_RATE_SECONDS = 30 # 30 seconds | ||
CONNS_PER_RATE = 50 # allow at most 50 connections in a 30s window (per IP) | ||
CONCUR_CONNS = 25 # allow at most 25 concurrent connections (per IP) | ||
|
||
# CTFd decay | ||
DECAY = 50 | ||
MAX_NODES = 1 | ||
# NODES_FQDN = "gke-nodes.internal" | ||
NODES_FQDN = "10.132.0.5" | ||
CTFD_LINK = "http://ctf.shellmates.club/" | ||
|
||
# challenge.py | ||
NODEPORT_START = 30000 | ||
PORT_MOD = 1000 | ||
PORTS_PATH = "config/ports.yml" | ||
YML_FILE = "challenge.yml" | ||
INDENT = 4 | ||
CHAL_DIRS = { | ||
"crypto", | ||
"forensics", | ||
"jail", | ||
"linux", | ||
"misc", | ||
"osint", | ||
"pwn", | ||
"reverse", | ||
"web", | ||
"general-skills", | ||
} | ||
AUTOBAN_DEFAULT = False | ||
FW_PRIORITY = 1000 | ||
FW_SOURCE_RANGES = "0.0.0.0/0" | ||
FW_TARGET_TAGS = "haproxy" | ||
FW_SKIP_CHECK = False | ||
DYNAMIC_CHAL_TYPE = "dynamic" | ||
DOCKER_CHAL_TYPE = "docker" | ||
PORT_PLACE_HOLDER = "${PORT}" | ||
PROJECT_ID_PLACE_HOLDER = "${PROJECT_ID}" | ||
|
||
# Override existing challenges in chals.json | ||
OVERRIDE_CHALS_JSON = True | ||
|
||
# Override existing firewall rules | ||
OVERRIDE_FW = False | ||
DEFAULT_DELETE_FW = False | ||
GCR_REPO = f"gcr.io/{PROJECT_ID}" | ||
DOCKERFILE_DIRS = { | ||
".", | ||
"challenge", | ||
"app", | ||
} | ||
DOCKERFILE_NAME = "Dockerfile" | ||
|
||
# ctfcli | ||
CTFCLI_CMD = "ctf" | ||
CTFCLI_CHAL_TRACKER_PATH = "config/ctfcli_chals.json" | ||
CHSTATE_VISIBLE = "visible" | ||
CHSTATE_HIDDEN = "hidden" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from conf import * | ||
import jinja2 | ||
from os import path | ||
import json | ||
import re | ||
from challenge import * | ||
import argparse | ||
import sys | ||
|
||
SCRIPT_ROOT = path.dirname(__file__) | ||
JINJA_ENV = jinja2.Environment(loader=jinja2.FileSystemLoader(path.join(SCRIPT_ROOT, TEMPLATES_DIR)), trim_blocks=True, lstrip_blocks=True) | ||
SUBDOMAIN_REGEX = "^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$" | ||
|
||
def argument_parser(): | ||
parser = argparse.ArgumentParser(description="Auto assign challenge ports, update HAProxy configuration and deploy challenges to Kubernetes") | ||
|
||
parser.add_argument("-c", "--challenges", metavar="CHALLENGE", nargs="*", help="Paths to challenges") | ||
parser.add_argument("-d", "--deploy", default=True, action="store_true", help="Assign port, deploy to HAProxy and Kubernetes, and create firewall rule (this is done by default)") | ||
parser.add_argument("-u", "--undeploy", action="store_true", help="Undeploy from HAProxy and Kubernetes, and disable or delete firewall rule (default is to disable)") | ||
parser.add_argument("--only-haproxy", action="store_true", help="Only update HAProxy configuration") | ||
parser.add_argument("-A", "--all", action="store_true", help="Apply to all challenges") | ||
parser.add_argument("--deploy-no-create-fw", action="store_true", help="Do not create firewall rule when using --deploy (default is to create)") | ||
parser.add_argument("--undeploy-delete-fw", action="store_true", help="Delete firewall rule when using --undeploy (default is to disable not delete)") | ||
parser.add_argument("--create-fw", action="store_true", help="Create firewall rule") | ||
parser.add_argument("--delete-fw", action="store_true", help="Delete firewall rule") | ||
parser.add_argument("--enable-fw", action="store_true", help="Enable firewall rule") | ||
parser.add_argument("--disable-fw", action="store_true", help="Disable firewall rule") | ||
parser.add_argument("--no-build", action="store_true", help="Do not build container image (default is to build)") | ||
parser.add_argument("--only-build", action="store_true", help="Only build container image") | ||
parser.add_argument("--only-push-ctfd", action="store_true", help="Only push to CTFd") | ||
parser.add_argument("--no-push-ctfd", action="store_true", help="Do not push to CTFd (default is to push)") | ||
parser.add_argument("--only-ctfcli-untrack", action="store_true", help="Untrack from ctfcli tracker") | ||
parser.add_argument("-s", "--state", choices=[CHSTATE_HIDDEN, CHSTATE_VISIBLE], help="Change visibility state on CTFd") | ||
parser.add_argument("-w", "--wave", metavar="WAVE", type=int, help="Apply to specified wave of challenges") | ||
parser.add_argument("--not-wave", metavar="WAVE", type=int, help="Apply to challenges not of specified wave") | ||
|
||
return parser | ||
|
||
def parse_chals(filename): | ||
with open(filename) as f: | ||
challenges = json.load(f) | ||
for chal in challenges.values(): | ||
subdomain = chal["subdomain"] or chal["name"] | ||
if not re.match(SUBDOMAIN_REGEX, subdomain): | ||
raise ValueError(f"'{subdomain}' does not match subdomain regular expression") | ||
return challenges.values() | ||
|
||
def haproxy_cfg(filename, challenges, directory): | ||
filepath = f"{directory}/{filename}" | ||
if path.exists(filepath): | ||
log(f"[!] Warning: '{filepath}' already exists") | ||
|
||
template = JINJA_ENV.get_template(f"{filename}.j2") | ||
content = template.render({ | ||
"challenges": challenges, | ||
"HTTP_HOSTS_MAP_PATH": HTTP_HOSTS_MAP_PATH, | ||
# "SNI_MAP_PATH": SNI_MAP_PATH, | ||
"TCP_TYPE": TCP_TYPE, | ||
"HTTP_TYPE": HTTP_TYPE, | ||
"NODES": NODES, | ||
"STATS_PORT": STATS_PORT, | ||
"STATS_USER": STATS_USER, | ||
"STATS_PASSWORD": STATS_PASSWORD, | ||
# "SSL_CERTIFICATE_PATH": SSL_CERTIFICATE_PATH, | ||
"IP_BAN_MINUTES": IP_BAN_MINUTES, | ||
"CONN_RATE_SECONDS": CONN_RATE_SECONDS, | ||
"CONNS_PER_RATE": CONNS_PER_RATE, | ||
"CONCUR_CONNS": CONCUR_CONNS, | ||
"MAX_NODES": MAX_NODES, | ||
"NODES_FQDN": NODES_FQDN, | ||
"CTFD_LINK": CTFD_LINK, | ||
}) | ||
with open(filepath, 'w') as f: | ||
f.write(content) | ||
|
||
def hosts_map(filename, challenges, directory): | ||
filepath = f"{directory}/{filename}" | ||
if path.exists(filepath): | ||
log(f"[!] Warning: '{filepath}' already exists") | ||
|
||
template = JINJA_ENV.get_template(f"{filename}.j2") | ||
content = template.render({ | ||
"challenges": challenges, | ||
"HTTP_TYPE": HTTP_TYPE, | ||
"DOMAIN_NAME": DOMAIN_NAME | ||
}) | ||
with open(filepath, 'w') as f: | ||
f.write(content) | ||
|
||
def update_haproxy(): | ||
# Generate HAProxy config files | ||
challenges = parse_chals(CHALLENGES_JSON_PATH) | ||
haproxy_cfg(HAPROXY_CFG, challenges, HAPROXY_CONFIG_DIR) | ||
hosts_map(HTTP_HOSTS_MAP, challenges, HAPROXY_CONFIG_DIR) | ||
# hosts_map(SNI_MAP, challenges, directory) | ||
|
||
# Copy the files to remote server and restart HAProxy | ||
gcloud_scp_l2r(f"{HAPROXY_CONFIG_DIR}/{HAPROXY_CFG}", HAPROXY_ROOT, HAPROXY_USER, INSTANCE_NAME, INSTANCE_ZONE) | ||
gcloud_scp_l2r(f"{HAPROXY_CONFIG_DIR}/{HTTP_HOSTS_MAP}", HTTP_HOSTS_MAP_PATH, HAPROXY_USER, INSTANCE_NAME, INSTANCE_ZONE) | ||
gcloud_ssh_cmd(HAPROXY_USER, INSTANCE_NAME, INSTANCE_ZONE, "systemctl restart haproxy; systemctl status haproxy") | ||
|
||
if __name__ == "__main__": | ||
parser = argument_parser() | ||
args = parser.parse_args() | ||
|
||
if args.only_haproxy: | ||
log(f"[!] Ignoring all other options as --only-haproxy was specified") | ||
update_haproxy() | ||
sys.exit(0) | ||
|
||
if args.deploy_no_create_fw and not args.deploy: | ||
log(f"[!] Ignoring option --deploy-no-create-fw as --deploy was not specified") | ||
if args.undeploy_delete_fw and not args.undeploy: | ||
log(f"[!] Ignoring option --undeploy-delete-fw as --undeploy was not specified") | ||
if args.create_fw and args.delete_fw: | ||
log(f"[-] Can only specify one of --create-fw and --delete-fw") | ||
sys.exit(1) | ||
if args.enable_fw and args.disable_fw: | ||
log(f"[-] Can only specify one of --enable-fw and --disable-fw") | ||
sys.exit(1) | ||
if args.wave and args.not_wave: | ||
log(f"[-] Can only specify one of --wave and --not-wave") | ||
sys.exit(1) | ||
|
||
if args.all: | ||
log(f"[*] Loading all challenges") | ||
chalpaths = get_chalpaths() | ||
elif args.wave: | ||
log(f"[*] Loading challenges of wave {args.wave}") | ||
chalpaths = get_chalpaths(wave=args.wave) | ||
elif args.not_wave: | ||
log(f"[*] Loading challenges not of wave {args.not_wave}") | ||
chalpaths = get_chalpaths(wave=args.not_wave, reverse=True) | ||
elif len(args.challenges) > 0: | ||
chalpaths = args.challenges | ||
else: | ||
log(f"[-] No challenge(s) specified, --wave not specified or --all not specified") | ||
sys.exit(1) | ||
|
||
if args.only_build: | ||
log(f"[!] Ignoring all other options as --only-build was specified") | ||
for chalpath in chalpaths: | ||
build_image(chalpath) | ||
sys.exit(0) | ||
|
||
if args.only_ctfcli_untrack: | ||
log(f"[!] Ignoring all other options as --only-ctfcli-untrack was specified") | ||
if args.all: | ||
ctfcli_untrack_all() | ||
else: | ||
for chalpath in chalpaths: | ||
ctfcli_untrack(chalpath) | ||
sys.exit(0) | ||
|
||
if args.only_push_ctfd: | ||
log(f"[!] Ignoring all other options as --only-push-ctfd was specified") | ||
for chalpath in chalpaths: | ||
ctfcli_push(chalpath, update=True) | ||
sys.exit(0) | ||
|
||
if args.state: | ||
log(f"[!] Ignoring all other options as --state was specified") | ||
for chalpath in chalpaths: | ||
ctfcli_chstate(chalpath, args.state) | ||
sys.exit(0) | ||
|
||
fw_action = None | ||
if args.create_fw: | ||
log(f"[!] Ignoring all other options as --create-fw was specified") | ||
fw_action = create_fw | ||
elif args.delete_fw: | ||
log(f"[!] Ignoring all other options as --delete-fw was specified") | ||
fw_action = delete_fw | ||
elif args.enable_fw: | ||
log(f"[!] Ignoring all other options as --enable-fw was specified") | ||
fw_action = enable_fw | ||
elif args.disable_fw: | ||
log(f"[!] Ignoring all other options as --disable-fw was specified") | ||
fw_action = disable_fw | ||
|
||
if fw_action is not None: | ||
for chalpath in chalpaths: | ||
fw_action(chalpath) | ||
sys.exit(0) | ||
|
||
build = not args.no_build | ||
deploy = args.deploy | ||
undeploy = args.undeploy | ||
createfw = not args.deploy_no_create_fw | ||
deletefw = args.undeploy_delete_fw | ||
push_ctfd = not args.no_push_ctfd | ||
|
||
if args.undeploy: | ||
undeploy_chals(chalpaths, deletefw) | ||
else: | ||
deploy_chals(chalpaths, build, deploy, createfw, push_ctfd) | ||
update_haproxy() |
Oops, something went wrong.