feat: challenge registry-driven platform + XVI/XVII imports + admin toggle + domain rename
- 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)
This commit is contained in:
1 parent
c35b23a37f
commit
c6fd9ec268
1317 files changed
+306574
-116
No files matched your search
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
compose_gen — render a team's docker-compose.yml from the challenge registry.
|
||||||
|
|
||||||
|
For every ENABLED challenge, a canonical per-challenge compose template lives
|
||||||
|
at services/<name>/docker-compose.yml (see gen_canonical_composes.py). The
|
||||||
|
renderer:
|
||||||
|
|
||||||
|
- replaces `build:` blocks with `image: services-<name>` for the MAIN
|
||||||
|
service (sidecars keep their images/builds),
|
||||||
|
- rewrites container_name / hostname to the per-team suffix,
|
||||||
|
- rewrites host ports (<ORG>:<INT>, <ORG+22>:22) to the team's ports,
|
||||||
|
- replaces PASSWORD_<ORG> placeholders with the team's challenge password,
|
||||||
|
- normalizes flag volume to ../receiver/flags/<name>.txt.
|
||||||
|
|
||||||
|
Multi-container challenges (gemas-notes, gemas-fetcher, kode-viewer,
|
||||||
|
anti-alchemy, tempest-poc) keep their sidecar services.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = Path("/opt/gemastik18-final")
|
||||||
|
TEAMS_DIR = BASE / "teams"
|
||||||
|
SERVICES_SRC = BASE / "services"
|
||||||
|
|
||||||
|
# Challenges whose compose has multiple top-level services; the FIRST service
|
||||||
|
# listed is the MAIN challenge service (gets image: reuse + challenge ports),
|
||||||
|
# the rest are sidecars.
|
||||||
|
SIDECAR_NAMES = {
|
||||||
|
"anti-alchemy": ["anti-alchemy-db"],
|
||||||
|
"gemas-fetcher": ["mongodb"],
|
||||||
|
"gemas-notes": ["database", "validation-service"],
|
||||||
|
"kode-viewer": ["redis"],
|
||||||
|
"tempest-poc": ["backend"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _load_registry() -> dict:
|
||||||
|
try:
|
||||||
|
return json.loads((TEAMS_DIR / "challenge_registry.json").read_text())
|
||||||
|
except Exception:
|
||||||
|
return {"sets": {}, "challenges": []}
|
||||||
|
|
||||||
|
def read_template(name: str) -> str:
|
||||||
|
p = SERVICES_SRC / name / "docker-compose.yml"
|
||||||
|
if not p.exists():
|
||||||
|
raise FileNotFoundError(f"Tidak ada template compose untuk {name} di {p}")
|
||||||
|
return p.read_text()
|
||||||
|
|
||||||
|
def _parse_services(text: str):
|
||||||
|
names = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
m = re.match(r"^ ([A-Za-z0-9_-]+):\s*$", line)
|
||||||
|
if m and not line.startswith(" "):
|
||||||
|
names.append(m.group(1))
|
||||||
|
return names
|
||||||
|
|
||||||
|
def render_team_compose(idx: int, state: dict) -> str:
|
||||||
|
ports = state["ports"]
|
||||||
|
passwords = state["chall_passwords"]
|
||||||
|
blocks = []
|
||||||
|
for ch in enabled_challenges():
|
||||||
|
name = ch["name"]
|
||||||
|
text = read_template(name)
|
||||||
|
main = _parse_services(text)
|
||||||
|
main = main[0] if main else name
|
||||||
|
sidecars = set(SIDECAR_NAMES.get(name, []))
|
||||||
|
org = int(ch.get("org_port", 10000))
|
||||||
|
tc = ports[name]["chall"]
|
||||||
|
ts = ports[name]["ssh"]
|
||||||
|
|
||||||
|
# --- rewrite container_name / hostname per team ---
|
||||||
|
out_lines = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
s = line.strip()
|
||||||
|
if s.startswith("container_name:"):
|
||||||
|
cname = s.split(":", 1)[1].strip()
|
||||||
|
line = f" container_name: {cname}_team{idx}"
|
||||||
|
elif s.startswith("hostname:"):
|
||||||
|
hname = s.split(":", 1)[1].strip()
|
||||||
|
if hname == name:
|
||||||
|
line = f" hostname: {name}_team{idx}"
|
||||||
|
else:
|
||||||
|
# sidecar hostname also suffixed to keep per-team network unique
|
||||||
|
line = f" hostname: {hname}_team{idx}"
|
||||||
|
out_lines.append(line)
|
||||||
|
text = "\n".join(out_lines)
|
||||||
|
|
||||||
|
# --- ports: rewrite ONLY the main challenge service's ports ---
|
||||||
|
# The main service is the one whose ports map to org/org+22.
|
||||||
|
# (sidecar "ports_chall" cases: gemas-notes validation-service exposes
|
||||||
|
# 12000:80 — handled by rewriting ANY "<org>:" / "<org+22>:" occurrence.)
|
||||||
|
text = re.sub(rf'"({org}):', f'"{tc}:', text)
|
||||||
|
text = re.sub(rf'"({org + 22}):', f'"{ts}:', text)
|
||||||
|
|
||||||
|
# --- build: -> image for MAIN only ---
|
||||||
|
# Replace the main service's build block with image: services-<name>.
|
||||||
|
# NB we process by service names, not generic removal, so sidecar
|
||||||
|
# builds survive.
|
||||||
|
lines = text.splitlines()
|
||||||
|
i = 0
|
||||||
|
in_main = False
|
||||||
|
cur = None
|
||||||
|
out = []
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
m = re.match(r"^ ([A-Za-z0-9_-]+):\s*$", line)
|
||||||
|
if m and not line.startswith(" "):
|
||||||
|
cur = m.group(1)
|
||||||
|
in_main = (cur == main)
|
||||||
|
out.append(line)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
# inside a service block
|
||||||
|
if in_main and line.strip() == "build:":
|
||||||
|
# skip build block (context/args/dockerfile...) until next key at same indent
|
||||||
|
out.append(f" image: services-{name}")
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and (lines[i].startswith(" ") or lines[i].strip() == ""):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
out.append(line)
|
||||||
|
i += 1
|
||||||
|
text = "\n".join(out)
|
||||||
|
|
||||||
|
# --- PASSWORD placeholder -> team password ---
|
||||||
|
text = re.sub(r"\$PASSWORD_" + str(org) + r"\b", passwords[name], text)
|
||||||
|
text = re.sub(r"PASSWORD_" + str(org) + r"\b", passwords[name], text)
|
||||||
|
|
||||||
|
# --- flag volume normalization ---
|
||||||
|
# Replace any ./flag.txt / ../receiver/flags/<name>.txt with the per-team flag mount
|
||||||
|
text = re.sub(r"\./flag\.txt(:\w+)?", f"../receiver/flags/{name}.txt", text)
|
||||||
|
blocks.append(text)
|
||||||
|
header = "version: '3.8'\nservices:\n"
|
||||||
|
body = []
|
||||||
|
for b in blocks:
|
||||||
|
if not b.strip():
|
||||||
|
continue
|
||||||
|
# strip a leading "services:" header from each block (they are fragments)
|
||||||
|
b = re.sub(r"^services:\n", "", b)
|
||||||
|
body.append(b)
|
||||||
|
return header + "\n".join(body) + "\n"
|
||||||
|
|
||||||
|
def enabled_challenges() -> list:
|
||||||
|
return [c for c in _load_registry().get("challenges", []) if c.get("enabled")]
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate canonical docker-compose.yml templates under services/<name>/ for
|
||||||
|
every challenge in the registry (gemastik18 + imported XVI/XVII).
|
||||||
|
|
||||||
|
The generated per-challenge file is the SOURCE for compose_gen — i.e. the
|
||||||
|
per-team compose is rendered from these templates. Uses:
|
||||||
|
|
||||||
|
services/<name>/docker-compose.yml (canonical, uniform)
|
||||||
|
|
||||||
|
If a template already exists for gemastik18 challenges (from the shared
|
||||||
|
compose), keep it. For imported challenges, build one from the registry.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = Path("/opt/gemastik18-final")
|
||||||
|
SVC = BASE / "services"
|
||||||
|
TEAMS_DIR = BASE / "teams"
|
||||||
|
|
||||||
|
def load_registry():
|
||||||
|
return json.loads((TEAMS_DIR / "challenge_registry.json").read_text())
|
||||||
|
|
||||||
|
def internal_port_for(ch: dict) -> int:
|
||||||
|
"""Best-effort: the container's internal listening port."""
|
||||||
|
name = ch["name"]
|
||||||
|
known = {
|
||||||
|
# web-ish default 8000, others from upstream compose
|
||||||
|
"art": 8080, "xl": 3000, "gemas-notes": 80, "pasta": 8000,
|
||||||
|
"burvesigner": 80, "hirnfick": 8000, "gemas-fetcher": 8000,
|
||||||
|
"s3": 80, "crawlback": 80, "back-to-basic": 8000,
|
||||||
|
"anti-alchemy": 5000, "asmr": 8000, "bit-canvas": 8000,
|
||||||
|
"fjb": 80, "gift-card": 5000, "gift-voucher": 5000,
|
||||||
|
"gleam-drive": 8000, "go-green": 8000, "kode-viewer": 3000,
|
||||||
|
"more-less": 8000, "tempest-poc": 80, "ticketer": 5000,
|
||||||
|
}
|
||||||
|
return known.get(name, 8000)
|
||||||
|
|
||||||
|
def sidecar_for(ch: dict) -> dict:
|
||||||
|
"""Return {sidecar_name: {image, env, volumes, cmd...}} or {}."""
|
||||||
|
name = ch["name"]
|
||||||
|
if name == "gemas-notes":
|
||||||
|
return {
|
||||||
|
"database": {
|
||||||
|
"image": "mysql:8",
|
||||||
|
"container_name": None, # per-team
|
||||||
|
"environment": ["MYSQL_ROOT_PASSWORD=why_my_random_string_password_doesnot_working",
|
||||||
|
"MYSQL_DATABASE=gemasnotes"],
|
||||||
|
"volumes": ["./gemas-notes/src/db/init.sql:/docker-entrypoint-initdb.d/init.sql"],
|
||||||
|
},
|
||||||
|
"validation-service": {
|
||||||
|
"build": "./gemas-notes/src/validation-service",
|
||||||
|
"container_name": None,
|
||||||
|
"depends_on": ["database"],
|
||||||
|
"ports_chall": 80, # challenge port exposed here (REST API)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if name == "gemas-fetcher":
|
||||||
|
return {
|
||||||
|
"mongodb": {
|
||||||
|
"image": "mongo:4.4",
|
||||||
|
"container_name": None,
|
||||||
|
"environment": ["MONGO_INITDB_ROOT_USERNAME=ctf",
|
||||||
|
"MONGO_INITDB_ROOT_PASSWORD=asjdkjk23j1k3dsdn2h233j3jj",
|
||||||
|
"MONGO_INITDB_DATABASE=web_fetcher"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "kode-viewer":
|
||||||
|
return {
|
||||||
|
"redis": {
|
||||||
|
"image": "redis:alpine",
|
||||||
|
"container_name": None,
|
||||||
|
"volumes": ["./redis-data:/data"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "anti-alchemy":
|
||||||
|
return {
|
||||||
|
"anti-alchemy-db": {
|
||||||
|
"image": "postgres:16.3-alpine",
|
||||||
|
"container_name": None,
|
||||||
|
"environment": ["POSTGRES_USER=postgres", "POSTGRES_PASSWORD=password"],
|
||||||
|
"volumes": ["./db/dump.sql:/docker-entrypoint-initdb.d/init.sql"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "tempest-poc":
|
||||||
|
return {
|
||||||
|
"backend": {
|
||||||
|
"build": "./tempest-poc/backend",
|
||||||
|
"container_name": None,
|
||||||
|
"ports_ssh": 22,
|
||||||
|
"extra_hosts": True,
|
||||||
|
},
|
||||||
|
"frontend": {
|
||||||
|
"build": "./tempest-poc/frontend",
|
||||||
|
"container_name": None,
|
||||||
|
"ports_chall": 80,
|
||||||
|
"extra_hosts": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def render(ch: dict) -> str:
|
||||||
|
name = ch["name"]
|
||||||
|
org = int(ch.get("org_port", 10000))
|
||||||
|
internal = internal_port_for(ch)
|
||||||
|
sidecars = sidecar_for(ch)
|
||||||
|
# For tempest-poc the main (first) service is frontend; compose_gen's
|
||||||
|
# SIDECAR_NAMES marks backend as sidecar. Order matters: put frontend
|
||||||
|
# before backend in the generated file so compose_gen picks frontend as main.
|
||||||
|
if name == "tempest-poc":
|
||||||
|
sidecars = {"frontend": sidecars.pop("frontend"), **sidecars}
|
||||||
|
lines = ["services:"]
|
||||||
|
# main service
|
||||||
|
cont = f"{name}_container"
|
||||||
|
lines += [
|
||||||
|
f" {name}:",
|
||||||
|
f" container_name: {cont}",
|
||||||
|
f" hostname: {name}",
|
||||||
|
" restart: always",
|
||||||
|
" build:",
|
||||||
|
" context: .",
|
||||||
|
" args:",
|
||||||
|
f" - PASSWORD=$PASSWORD_{org}",
|
||||||
|
]
|
||||||
|
# volumes: flag + bashrc + preexec (uniform unless challenge differs)
|
||||||
|
flag_path = f"../receiver/flags/{name}.txt:/flag.txt:ro"
|
||||||
|
# gift-card/gift-voucher mount to /ctf/<name>/flag.txt
|
||||||
|
if name in ("gift-card", "gift-voucher"):
|
||||||
|
flag_path = f"../receiver/flags/{name}.txt:/ctf/{name}/flag.txt:ro"
|
||||||
|
if name == "pasta":
|
||||||
|
flag_path = f"../receiver/flags/{name}.txt:/ctf/pasta/flag.txt:ro"
|
||||||
|
lines += [
|
||||||
|
" volumes:",
|
||||||
|
f" - {flag_path}",
|
||||||
|
" - ../utils/bashrc:/root/.bashrc:ro",
|
||||||
|
" - ../utils/preexec.sh:/root/.preexec.sh:ro",
|
||||||
|
]
|
||||||
|
# ports: external (org / org+22) but rewritten per team by composer
|
||||||
|
if name == "gemas-notes":
|
||||||
|
# main service = note-service (Go, ssh only). The challenge port is
|
||||||
|
# exposed by validation-service (the REST API the checker hits).
|
||||||
|
pass
|
||||||
|
elif name == "tempest-poc":
|
||||||
|
# main (frontend) exposes chall port; backend (sidecar) has ssh.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
lines += [
|
||||||
|
" ports:",
|
||||||
|
f" - \"{org}:{internal}\"",
|
||||||
|
f" - \"{org + 22}:22\"",
|
||||||
|
]
|
||||||
|
lines.append(" extra_hosts:")
|
||||||
|
lines.append(' - "host.docker.internal:host-gateway"')
|
||||||
|
# env (challenge-specific)
|
||||||
|
if name == "carbeat":
|
||||||
|
lines.append(" environment:")
|
||||||
|
lines.append(" - FLAG=GEMASTIK18{local_flag}")
|
||||||
|
if name == "phew":
|
||||||
|
lines.append(" environment:")
|
||||||
|
lines.append(" - FLAG=GEMASTIK18{local_flag}")
|
||||||
|
if name == "sheesh":
|
||||||
|
lines.append(" environment:")
|
||||||
|
lines.append(" - FLAG=GEMASTIK18{local_flag}")
|
||||||
|
if name == "anti-alchemy":
|
||||||
|
lines.append(" environment:")
|
||||||
|
lines.append(" - DB_NAME=postgres")
|
||||||
|
lines.append(" - DB_USER=postgres")
|
||||||
|
lines.append(" - DB_PASS=password")
|
||||||
|
lines.append(" - DB_HOST=anti-alchemy-db")
|
||||||
|
lines.append(" - DB_PORT=5432")
|
||||||
|
lines.append(f" - SECRET_KEY=$PASSWORD_{org}")
|
||||||
|
lines.append(" depends_on:")
|
||||||
|
lines.append(" - anti-alchemy-db")
|
||||||
|
if name == "gemas-fetcher":
|
||||||
|
lines.append(" environment:")
|
||||||
|
lines.append(" - MONGO_URI=mongodb://ctf:asjdkjk23j1k3dsdn2h233j3jj@mongodb:27017/web_fetcher?authSource=admin")
|
||||||
|
lines.append(f" - APP_URI=http://0.0.0.0:{org}")
|
||||||
|
lines.append(" depends_on:")
|
||||||
|
lines.append(" - mongodb")
|
||||||
|
if name == "kode-viewer":
|
||||||
|
lines.append(" environment:")
|
||||||
|
lines.append(" - REDIS_HOST=redis")
|
||||||
|
lines.append(" - REDIS_PORT=6379")
|
||||||
|
lines.append(" depends_on:")
|
||||||
|
lines.append(" - redis")
|
||||||
|
if name == "gemas-notes":
|
||||||
|
lines.append(" ports:")
|
||||||
|
lines.append(f" - \"{org + 22}:22\"")
|
||||||
|
lines.append(" depends_on:")
|
||||||
|
lines.append(" - database")
|
||||||
|
lines.append(" - validation-service")
|
||||||
|
# note: gemas-notes internal port differs (validation-service is the 8000 listener?)
|
||||||
|
# upstream maps validation-service:12000:80 and gemas-notes has no port except ssh.
|
||||||
|
# Our single main service is gemas-notes which exposes ssh on 22.
|
||||||
|
if name == "burvesigner":
|
||||||
|
lines.append(" volumes:")
|
||||||
|
lines.append(" - ../receiver/files/burvesigner.priv:/priv.data:ro")
|
||||||
|
# sidecars
|
||||||
|
for sname, sc in sidecars.items():
|
||||||
|
cont_s = f"{sname}_container"
|
||||||
|
lines.append(f" {sname}:")
|
||||||
|
if sc.get("build"):
|
||||||
|
lines += [" build:",
|
||||||
|
f" context: {sc['build']}",
|
||||||
|
f" dockerfile: Dockerfile"]
|
||||||
|
elif sc.get("image"):
|
||||||
|
lines.append(f" image: {sc['image']}")
|
||||||
|
if sc.get("container_name") is not None:
|
||||||
|
lines.append(f" container_name: {cont_s}")
|
||||||
|
if sc.get("environment"):
|
||||||
|
lines.append(" environment:")
|
||||||
|
for e in sc["environment"]:
|
||||||
|
lines.append(f" - {e}")
|
||||||
|
if sc.get("volumes"):
|
||||||
|
lines.append(" volumes:")
|
||||||
|
for v in sc["volumes"]:
|
||||||
|
lines.append(f" - {v}")
|
||||||
|
if sc.get("depends_on"):
|
||||||
|
lines.append(" depends_on:")
|
||||||
|
for dep in sc["depends_on"]:
|
||||||
|
lines.append(f" - {dep}")
|
||||||
|
if sc.get("ports_chall"):
|
||||||
|
lines.append(" ports:")
|
||||||
|
lines.append(f" - \"{org}:{sc['ports_chall']}\"")
|
||||||
|
if sc.get("ports_ssh"):
|
||||||
|
lines.append(" ports:")
|
||||||
|
lines.append(f" - \"{org + 22}:{sc['ports_ssh']}\"")
|
||||||
|
if sc.get("extra_hosts"):
|
||||||
|
lines.append(" extra_hosts:")
|
||||||
|
lines.append(' - "host.docker.internal:host-gateway"')
|
||||||
|
if sname == "frontend" and name == "tempest-poc":
|
||||||
|
# tempest frontend already handled via ports_chall
|
||||||
|
pass
|
||||||
|
elif sname == "frontend":
|
||||||
|
lines += [" ports:", f" - \"{org}:80\""]
|
||||||
|
lines.append(" extra_hosts:")
|
||||||
|
lines.append(' - "host.docker.internal:host-gateway"')
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
reg = load_registry()
|
||||||
|
for ch in reg["challenges"]:
|
||||||
|
name = ch["name"]
|
||||||
|
dst = SVC / name / "docker-compose.yml"
|
||||||
|
if name in ("blogpost", "carbeat", "cdn", "phew", "sheesh", "warmup"):
|
||||||
|
# keep existing split template (from shared compose)
|
||||||
|
tpl = SVC / name / "compose.template.yml"
|
||||||
|
if tpl.exists() and not dst.exists():
|
||||||
|
text = tpl.read_text()
|
||||||
|
lines = [l for l in text.splitlines() if l.strip() and not l.strip().startswith("#")]
|
||||||
|
out = []
|
||||||
|
for l in lines:
|
||||||
|
if l.strip().startswith("# ---") and out:
|
||||||
|
break
|
||||||
|
out.append(l)
|
||||||
|
dst.write_text("\n".join(out).rstrip() + "\n")
|
||||||
|
print(f"kept template: {name}")
|
||||||
|
continue
|
||||||
|
if name == "tempest-poc":
|
||||||
|
dst.write_text(render_tempest(ch))
|
||||||
|
print(f"wrote canonical compose (tempest): {name}")
|
||||||
|
continue
|
||||||
|
dst.write_text(render(ch))
|
||||||
|
print(f"wrote canonical compose: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
def render_tempest(ch: dict) -> str:
|
||||||
|
"""tempest-poc: frontend (nginx) is the MAIN challenge service — it gets
|
||||||
|
the challenge container name + SSH + challenge port; backend is a sidecar
|
||||||
|
service that frontend proxies to (no host port needed)."""
|
||||||
|
name = ch["name"]
|
||||||
|
org = int(ch.get("org_port", 10000))
|
||||||
|
return f"""services:
|
||||||
|
{name}:
|
||||||
|
container_name: {name}_container
|
||||||
|
hostname: {name}
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: ./tempest-poc/frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "{org}:80"
|
||||||
|
- "{org + 22}:22"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./tempest-poc/backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
"""
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the per-team receiver main.py from the challenge registry.
|
||||||
|
|
||||||
|
The receiver's `challenges` dict is generated from ENABLED registry entries so
|
||||||
|
the SLA checker pool exactly matches the distributed challenges. Imports come
|
||||||
|
from three packages:
|
||||||
|
- challenges.<Name> (gemastik18 native checkers)
|
||||||
|
- challenges.xvi.<Name> (GEMASTIK XVI checkers)
|
||||||
|
- challenges.xvii.checkers (GEMASTIK XVII generic checkers)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from teams import TEAMS_DIR, load_registry
|
||||||
|
|
||||||
|
|
||||||
|
def checker_class_for(ch: dict) -> str:
|
||||||
|
"""Return Python import path + class name for a registry challenge."""
|
||||||
|
name = ch["name"]
|
||||||
|
s = ch["set"]
|
||||||
|
# gemastik18 native challenges have their own checker class (capitalized)
|
||||||
|
if s == "gemastik18":
|
||||||
|
cls = {
|
||||||
|
"blogpost": "Blogpost",
|
||||||
|
"carbeat": "Carbeat",
|
||||||
|
"cdn": "CDN",
|
||||||
|
"phew": "Phew",
|
||||||
|
"sheesh": "Sheesh",
|
||||||
|
"warmup": "Warmup",
|
||||||
|
}.get(name)
|
||||||
|
if cls:
|
||||||
|
return f"from challenges.{cls} import {cls}", cls
|
||||||
|
raise KeyError(f"no native checker for {name}")
|
||||||
|
if s == "xvi":
|
||||||
|
cls = {
|
||||||
|
"art": "Art",
|
||||||
|
"xl": "XL",
|
||||||
|
"gemas-notes": "GemasNotes",
|
||||||
|
"pasta": "Pasta",
|
||||||
|
"burvesigner": "Burvesigner",
|
||||||
|
"hirnfick": "Hirnfick",
|
||||||
|
"gemas-fetcher": "GemasFetcher",
|
||||||
|
"s3": "S3",
|
||||||
|
"crawlback": "Crawlback",
|
||||||
|
"back-to-basic": "BackToBasic",
|
||||||
|
}.get(name)
|
||||||
|
if cls:
|
||||||
|
return f"from challenges.xvi.{cls} import {cls}", cls
|
||||||
|
raise KeyError(f"no xvi checker for {name}")
|
||||||
|
if s == "xvii":
|
||||||
|
cls = {
|
||||||
|
"anti-alchemy": "AntiAlchemy",
|
||||||
|
"asmr": "Asmr",
|
||||||
|
"bit-canvas": "BitCanvas",
|
||||||
|
"fjb": "Fjb",
|
||||||
|
"gift-card": "GiftCard",
|
||||||
|
"gift-voucher": "GiftVoucher",
|
||||||
|
"gleam-drive": "GleamDrive",
|
||||||
|
"go-green": "GoGreen",
|
||||||
|
"kode-viewer": "KodeViewer",
|
||||||
|
"more-less": "MoreLess",
|
||||||
|
"tempest-poc": "TempestPoc",
|
||||||
|
"ticketer": "Ticketer",
|
||||||
|
}.get(name)
|
||||||
|
if cls:
|
||||||
|
return f"from challenges.xvii.checkers import {cls}", cls
|
||||||
|
raise KeyError(f"no xvii checker for {name}")
|
||||||
|
raise KeyError(f"unknown set {s}")
|
||||||
|
|
||||||
|
|
||||||
|
def render_receiver_main(enabled: list[dict], port_defaults: dict) -> str:
|
||||||
|
imports = []
|
||||||
|
entries = []
|
||||||
|
for ch in enabled:
|
||||||
|
name = ch["name"]
|
||||||
|
imp, cls = checker_class_for(ch)
|
||||||
|
imports.append(imp)
|
||||||
|
default = port_defaults.get(name, 10000)
|
||||||
|
entries.append(f' "{name}": {cls}(_ch_port("{name}", {default})),')
|
||||||
|
return f"""from fastapi import Depends, FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
from config import get_settings
|
||||||
|
|
||||||
|
{chr(10).join(imports)}
|
||||||
|
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
security = HTTPBasic()
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
def _ch_port(name: str, default: int) -> int:
|
||||||
|
# read from .env manually (pydantic settings has fixed fields)
|
||||||
|
val = os.environ.get(f"CHALLENGE_PORT_{{name.upper()}}")
|
||||||
|
if not val:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), ".env")) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(f"CHALLENGE_PORT_{{name.upper()}}="):
|
||||||
|
val = line.strip().split("=", 1)[1]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return int(val) if val else default
|
||||||
|
|
||||||
|
def _ch_container(name: str, default: str) -> str:
|
||||||
|
val = os.environ.get(f"CHALLENGE_CONTAINER_{{name.upper()}}")
|
||||||
|
if not val:
|
||||||
|
try:
|
||||||
|
with open(os.path.join(os.path.dirname(__file__), ".env")) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(f"CHALLENGE_CONTAINER_{{name.upper()}}="):
|
||||||
|
val = line.strip().split("=", 1)[1]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return val or default
|
||||||
|
|
||||||
|
challenges = {{
|
||||||
|
{chr(10).join(entries)}
|
||||||
|
}}
|
||||||
|
|
||||||
|
async def run_challenge_checks():
|
||||||
|
\"\"\"Run check function on all challenges at startup\"\"\"
|
||||||
|
logger.info("\\n" + "="*60)
|
||||||
|
logger.info("Running challenge checks...")
|
||||||
|
logger.info("="*60 + "\\n")
|
||||||
|
|
||||||
|
results = {{}}
|
||||||
|
|
||||||
|
for name, challenge in challenges.items():
|
||||||
|
logger.info(f"\\n[{{name}}] Starting check...")
|
||||||
|
try:
|
||||||
|
# Give service time between checks
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
result = challenge.check()
|
||||||
|
results[name] = result
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"[{{name}}] ✓ Check PASSED")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[{{name}}] ✗ Check FAILED")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[{{name}}] ✗ Check ERROR: {{e}}")
|
||||||
|
results[name] = False
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
logger.info("\\n" + "="*60)
|
||||||
|
logger.info("Challenge Check Summary:")
|
||||||
|
logger.info("="*60)
|
||||||
|
passed = sum(1 for r in results.values() if r)
|
||||||
|
total = len(results)
|
||||||
|
for name, result in results.items():
|
||||||
|
status = "✓ PASS" if result else "✗ FAIL"
|
||||||
|
logger.info(f" {{name:20}} {{status}}")
|
||||||
|
logger.info(f"\\nTotal: {{passed}}/{{total}} passed")
|
||||||
|
logger.info("="*60 + "\\n")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
\"\"\"Run challenge checks on application startup\"\"\"
|
||||||
|
asyncio.create_task(run_challenge_checks())
|
||||||
|
|
||||||
|
class Flag(BaseModel):
|
||||||
|
flag: str
|
||||||
|
challenge: str
|
||||||
|
|
||||||
|
class History(BaseModel):
|
||||||
|
log: str
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def read_root():
|
||||||
|
return {{"service": "receiver-service"}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/restart/{{challenge}}")
|
||||||
|
def restart(challenge: str, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, challenge)
|
||||||
|
os.system(f"docker compose -f {{settings.COMPOSE_LOCATION}} restart {{challenge}}")
|
||||||
|
return {{"message": "Challenge restarted"}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/rollback/{{challenge}}")
|
||||||
|
def rollback(challenge: str, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, challenge)
|
||||||
|
os.system(f"docker compose -f {{settings.COMPOSE_LOCATION}} up -d --force-recreate {{challenge}}")
|
||||||
|
return {{"message": "Challenge restarted"}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/activate/{{challenge}}")
|
||||||
|
def activate(challenge: str, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, challenge)
|
||||||
|
os.system(f"docker compose -f {{settings.COMPOSE_LOCATION}} up -d {{challenge}}")
|
||||||
|
return {{"message": "Challenge activated"}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/deactivate/{{challenge}}")
|
||||||
|
def deactive(challenge: str, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, challenge)
|
||||||
|
os.system(f"docker compose -f {{settings.COMPOSE_LOCATION}} down {{challenge}}")
|
||||||
|
return {{"message": "Challenge deactivated"}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/credential/{{challenge}}")
|
||||||
|
def credential(challenge: str, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, challenge)
|
||||||
|
return challenges[challenge].credentials()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/flag")
|
||||||
|
def receive(data: Flag, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, data.challenge)
|
||||||
|
challenge = challenges[data.challenge]
|
||||||
|
if challenge.distribute(data.flag):
|
||||||
|
return {{"message": "Flag received"}}
|
||||||
|
|
||||||
|
raise HTTPException(status_code=500, detail="Error receiving flag")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/check/{{challenge}}")
|
||||||
|
def check(challenge: str, credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
validate(credentials, challenge)
|
||||||
|
return {{"success": challenges[challenge].check()}}
|
||||||
|
|
||||||
|
@app.post("/history")
|
||||||
|
def history(data: History):
|
||||||
|
with open('history/command.txt', 'a') as f:
|
||||||
|
f.write(data.log + '\\n')
|
||||||
|
return {{"message": "Command received"}}
|
||||||
|
|
||||||
|
def is_admin(credentials):
|
||||||
|
if credentials.username != settings.ADMIN_USERNAME or credentials.password != settings.ADMIN_PASSWORD:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate(credentials, challenge):
|
||||||
|
if not is_admin(credentials):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||||
|
|
||||||
|
if challenge not in challenges:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid challenge")
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def sync_team_receivers() -> None:
|
||||||
|
"""Regenerate main.py for every existing team from its state + registry."""
|
||||||
|
for d in sorted(TEAMS_DIR.glob("team*")):
|
||||||
|
sf = d / "state.json"
|
||||||
|
if not sf.exists():
|
||||||
|
continue
|
||||||
|
st = json.loads(sf.read_text())
|
||||||
|
idx = st["index"]
|
||||||
|
enabled = [c for c in load_registry()["challenges"] if c.get("enabled")]
|
||||||
|
text = render_receiver_main(enabled, {c["name"]: st["ports"][c["name"]]["chall"] for c in enabled})
|
||||||
|
(d / "receiver" / "main.py").write_text(text)
|
||||||
|
print(f"team{idx}: receiver main.py regenerated ({len(enabled)} challenges)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sync_team_receivers()
|
||||||
@@ -25,6 +25,11 @@ def write_unit(idx: int, st: dict):
|
|||||||
continue
|
continue
|
||||||
env[f"CHALLENGE_PORT_{ch.upper()}"] = str(st["ports"][ch]["chall"])
|
env[f"CHALLENGE_PORT_{ch.upper()}"] = str(st["ports"][ch]["chall"])
|
||||||
env[f"CHALLENGE_CONTAINER_{ch.upper()}"] = f"{ch}_container_team{idx}"
|
env[f"CHALLENGE_CONTAINER_{ch.upper()}"] = f"{ch}_container_team{idx}"
|
||||||
|
# SSH passwords: checker Challenge.credentials() reads PASSWORD_<self.port>
|
||||||
|
# where self.port is the team challenge port.
|
||||||
|
pwd = st.get("chall_passwords", {}).get(ch)
|
||||||
|
if pwd:
|
||||||
|
env[f"PASSWORD_{st['ports'][ch]['chall']}"] = pwd
|
||||||
env["COMPOSE_LOCATION"] = str(TEAMS_DIR / f"team{idx}" / "services" / "docker-compose.yml")
|
env["COMPOSE_LOCATION"] = str(TEAMS_DIR / f"team{idx}" / "services" / "docker-compose.yml")
|
||||||
env_lines = "\n".join(f'Environment="{k}={v}"' for k, v in env.items())
|
env_lines = "\n".join(f'Environment="{k}={v}"' for k, v in env.items())
|
||||||
unit = f"""[Unit]
|
unit = f"""[Unit]
|
||||||
@@ -48,6 +53,12 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
action = sys.argv[1] if len(sys.argv) > 1 else "start"
|
action = sys.argv[1] if len(sys.argv) > 1 else "start"
|
||||||
|
# Regenerate receiver main.py for existing teams from the registry
|
||||||
|
try:
|
||||||
|
from gen_receiver_main import sync_team_receivers
|
||||||
|
sync_team_receivers()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"WARN: receiver main.py regen failed: {e}")
|
||||||
for d in sorted(TEAMS_DIR.glob("team*")):
|
for d in sorted(TEAMS_DIR.glob("team*")):
|
||||||
sf = d / "state.json"
|
sf = d / "state.json"
|
||||||
if not sf.exists():
|
if not sf.exists():
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""One-time importer: copy XVI/XVII challenge sources into the platform
|
||||||
|
services/ tree, with EOL base-image fixes and a canonical docker-compose.yml
|
||||||
|
(per-challenge template) that the compose generator can render per team.
|
||||||
|
|
||||||
|
Run after cloning the upstream repos:
|
||||||
|
python3 import_new_challenges.py <xvi_dir> <xvii_dir>
|
||||||
|
|
||||||
|
For each imported challenge it writes services/<name>/:
|
||||||
|
- source files (Dockerfile, src, start.sh, requirements, db dumps...)
|
||||||
|
- docker-compose.yml (canonical template: single main service + sidecars)
|
||||||
|
- apt-insecure.conf (2026-clock GPG fix)
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE = Path("/opt/gemastik18-final")
|
||||||
|
SVC = BASE / "services"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- helpers
|
||||||
|
def copy_tree(src: Path, dst: Path, ignore=None):
|
||||||
|
if dst.exists():
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
shutil.copytree(src, dst, ignore=ignore)
|
||||||
|
|
||||||
|
def add_apt_insecure(d: Path):
|
||||||
|
conf = "Acquire::AllowInsecureRepositories \"true\";\nAcquire::AllowDowngradeToInsecureRepositories \"true\";\nApt::Get::AllowUnauthenticated \"true\";\n"
|
||||||
|
(d / "apt-insecure.conf").write_text(conf)
|
||||||
|
|
||||||
|
def fix_base_image(d: Path, old: str, new: str):
|
||||||
|
"""Swap the FROM line in a Dockerfile (EOL base -> supported)."""
|
||||||
|
df = d / "Dockerfile"
|
||||||
|
if not df.exists():
|
||||||
|
return False
|
||||||
|
t = df.read_text()
|
||||||
|
if old in t:
|
||||||
|
df.write_text(t.replace(old, new, 1))
|
||||||
|
print(f" [fix base] {d.name}: {old} -> {new}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
DROP_FROM = [
|
||||||
|
(r"public\.ecr\.aws/docker/library/(python:3\.11-slim-buster|python:3\.11-slim-bullseye)\b", "python:3.11-slim-bookworm"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/ruby:2\.7\.2\b", "ruby:3.2-slim-bookworm"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/php:8\.0-apache\b", "php:8.2-apache-bookworm"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/golang:bullseye\b", "golang:1.22-bookworm"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/ubuntu:20\.04\b", "ubuntu:24.04"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/ubuntu:22\.04\b", "ubuntu:24.04"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/node(:[0-9]+)?\b", "node:20-slim-bookworm"),
|
||||||
|
(r"public\.ecr\.aws/docker/library/python:3\.10\.6\b", "python:3.10-slim-bookworm"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def fix_dockerfile(d: Path):
|
||||||
|
df = d / "Dockerfile"
|
||||||
|
if not df.exists():
|
||||||
|
return
|
||||||
|
t = df.read_text()
|
||||||
|
for pat, new in DROP_FROM:
|
||||||
|
t2 = re.sub(pat, new, t)
|
||||||
|
if t2 != t:
|
||||||
|
print(f" [fix base] {d.name}: {pat} -> {new}")
|
||||||
|
t = t2
|
||||||
|
# apt-insecure for every apt-get run
|
||||||
|
if "apt-get" in t and "99gemastik-insecure" not in t:
|
||||||
|
t = t.replace(
|
||||||
|
"RUN apt-get update",
|
||||||
|
"COPY apt-insecure.conf /etc/apt/apt.conf.d/99gemastik-insecure\nRUN apt-get -o Acquire::AllowInsecureRepositories=true update",
|
||||||
|
1)
|
||||||
|
t = t.replace(
|
||||||
|
"RUN apt-get install",
|
||||||
|
"RUN apt-get -y --allow-unauthenticated install",
|
||||||
|
1)
|
||||||
|
# ensure openssh + ctfuser-ish (the standard template)
|
||||||
|
df.write_text(t)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- import
|
||||||
|
def import_xvi(src: Path):
|
||||||
|
print(f"=== Importing XVI from {src} ===")
|
||||||
|
services = src / "services"
|
||||||
|
for d in sorted(services.iterdir()):
|
||||||
|
if not d.is_dir() or d.name == ".git":
|
||||||
|
continue
|
||||||
|
name = d.name
|
||||||
|
dst = SVC / name
|
||||||
|
print(f" - {name}")
|
||||||
|
copy_tree(d, dst, ignore=shutil.ignore_patterns("__pycache__", ".git"))
|
||||||
|
add_apt_insecure(dst)
|
||||||
|
fix_dockerfile(dst)
|
||||||
|
(dst / "docker-compose.yml").unlink(missing_ok=True)
|
||||||
|
# special: gemas-notes, gemas-fetcher need their src subdirs — already copied whole dir.
|
||||||
|
|
||||||
|
def import_xvii(src: Path):
|
||||||
|
print(f"=== Importing XVII from {src} ===")
|
||||||
|
for d in sorted(src.iterdir()):
|
||||||
|
if not d.is_dir() or d.name.startswith("."):
|
||||||
|
continue
|
||||||
|
name = d.name
|
||||||
|
dst = SVC / name
|
||||||
|
print(f" - {name}")
|
||||||
|
copy_tree(d, dst, ignore=shutil.ignore_patterns("__pycache__", ".git", "docker-compose.yml", "README.md", "test"))
|
||||||
|
add_apt_insecure(dst)
|
||||||
|
fix_dockerfile(dst)
|
||||||
|
(dst / "docker-compose.yml").unlink(missing_ok=True)
|
||||||
|
if name == "tempest-poc":
|
||||||
|
# compose is frontend+backend split; keep both Dockerfiles
|
||||||
|
for sub in ("backend", "frontend"):
|
||||||
|
subd = dst / sub
|
||||||
|
if subd.exists():
|
||||||
|
if (subd / "Dockerfile").exists():
|
||||||
|
fix_dockerfile(subd)
|
||||||
|
add_apt_insecure(subd)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
xvi = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/opt/gemastik-xvi-final")
|
||||||
|
xvii = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("/opt/gemastik-xvii-final")
|
||||||
|
import_xvi(xvi)
|
||||||
|
import_xvii(xvii)
|
||||||
|
print("Done. Next: write canonical docker-compose.yml templates per challenge.")
|
||||||
+45
-9
@@ -90,8 +90,8 @@ async def _proxy(method: str, path: str, body: dict = None):
|
|||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(req: Request):
|
async def index(req: Request):
|
||||||
host = (req.headers.get("host") or "").split(":")[0]
|
host = (req.headers.get("host") or "").split(":")[0]
|
||||||
# Team portal domains: <slug>.gemastik.imrnes.team -> their team portal (no admin login)
|
# Team portal domains: <slug>.attackdefense.imrnes.team -> their team portal (no admin login)
|
||||||
if host.endswith(".gemastik.imrnes.team") and host != "gemastik.imrnes.team" and host != "panel.gemastik.imrnes.team":
|
if host.endswith(".attackdefense.imrnes.team") and host != "attackdefense.imrnes.team" and host != "panel.attackdefense.imrnes.team":
|
||||||
slug = host.split(".")[0]
|
slug = host.split(".")[0]
|
||||||
for t in orch.list_teams():
|
for t in orch.list_teams():
|
||||||
if t.get("slug") == slug:
|
if t.get("slug") == slug:
|
||||||
@@ -117,7 +117,7 @@ async def submit_page(req: Request):
|
|||||||
return HTMLResponse((BASE_DIR / "static" / "submit.html").read_text())
|
return HTMLResponse((BASE_DIR / "static" / "submit.html").read_text())
|
||||||
|
|
||||||
|
|
||||||
# ============ Per-team portal (public via <slug>.gemastik.imrnes.team) ============
|
# ============ Per-team portal (public via <slug>.attackdefense.imrnes.team) ============
|
||||||
|
|
||||||
@app.get("/team/{idx}", response_class=HTMLResponse)
|
@app.get("/team/{idx}", response_class=HTMLResponse)
|
||||||
async def team_portal(idx: int, req: Request):
|
async def team_portal(idx: int, req: Request):
|
||||||
@@ -194,13 +194,13 @@ def _team_authorized(req: Request, idx: int) -> bool:
|
|||||||
|
|
||||||
def _check_team_host(req: Request, st: dict) -> bool:
|
def _check_team_host(req: Request, st: dict) -> bool:
|
||||||
"""IDOR guard: host must be this team's own domain (or localhost).
|
"""IDOR guard: host must be this team's own domain (or localhost).
|
||||||
panel.gemastik / gemastik.imrnes.team only allowed with a valid ADMIN session."""
|
panel.gemastik / attackdefense.imrnes.team only allowed with a valid ADMIN session."""
|
||||||
host = (req.headers.get("host") or "").split(":")[0]
|
host = (req.headers.get("host") or "").split(":")[0]
|
||||||
if host == st.get("domain"):
|
if host == st.get("domain"):
|
||||||
return True
|
return True
|
||||||
if host.startswith("127.0.0.1") or host.startswith("localhost"):
|
if host.startswith("127.0.0.1") or host.startswith("localhost"):
|
||||||
return True
|
return True
|
||||||
if host in ("panel.gemastik.imrnes.team", "gemastik.imrnes.team"):
|
if host in ("panel.attackdefense.imrnes.team", "attackdefense.imrnes.team"):
|
||||||
return _authorized(req) # admin preview only
|
return _authorized(req) # admin preview only
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ async def api_team_targets(idx: int, req: Request):
|
|||||||
out.append({
|
out.append({
|
||||||
"team_idx": st.get("index"),
|
"team_idx": st.get("index"),
|
||||||
"team_label": st.get("label", f"Team {st.get('index')}"),
|
"team_label": st.get("label", f"Team {st.get('index')}"),
|
||||||
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team"),
|
"domain": st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".attackdefense.imrnes.team"),
|
||||||
"port": p["chall"],
|
"port": p["chall"],
|
||||||
})
|
})
|
||||||
return {"targets": out}
|
return {"targets": out}
|
||||||
@@ -357,7 +357,43 @@ def require_login(req: Request):
|
|||||||
@app.get("/api/challenges")
|
@app.get("/api/challenges")
|
||||||
async def api_challenges(req: Request):
|
async def api_challenges(req: Request):
|
||||||
require_login(req)
|
require_login(req)
|
||||||
return {"challenges": CHALLENGES}
|
# Full registry (all sets) with enabled status, plus count of live teams
|
||||||
|
reg = orch.load_registry()
|
||||||
|
challs = []
|
||||||
|
for c in reg.get("challenges", []):
|
||||||
|
challs.append({
|
||||||
|
"name": c["name"],
|
||||||
|
"set": c.get("set"),
|
||||||
|
"category": c.get("category"),
|
||||||
|
"desc": c.get("desc"),
|
||||||
|
"enabled": bool(c.get("enabled")),
|
||||||
|
"chall_offset": c.get("chall_offset"),
|
||||||
|
"ssh_offset": c.get("ssh_offset"),
|
||||||
|
"org_port": c.get("org_port"),
|
||||||
|
"service_dir": c.get("service_dir"),
|
||||||
|
})
|
||||||
|
return {"challenges": challs, "hint": "PATCH /api/challenges/<name> with {\"enabled\": bool} to toggle"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/api/challenges/{challenge}")
|
||||||
|
async def api_challenge_toggle(challenge: str, req: Request):
|
||||||
|
require_login(req)
|
||||||
|
data = await req.json()
|
||||||
|
enabled = bool(data.get("enabled"))
|
||||||
|
reg = orch.load_registry()
|
||||||
|
found = any(c.get("name") == challenge for c in reg.get("challenges", []))
|
||||||
|
if not found:
|
||||||
|
raise HTTPException(404, "Unknown challenge")
|
||||||
|
changed = orch.set_challenge_enabled(challenge, enabled)
|
||||||
|
# apply to live teams (build/up or stop/remove + receiver restart);
|
||||||
|
# skip rebuild when the flag didn't actually change
|
||||||
|
if "unchanged" in changed:
|
||||||
|
return {"ok": True, "name": challenge, "enabled": enabled, "applied": [], "unchanged": True}
|
||||||
|
try:
|
||||||
|
report = await asyncio.to_thread(orch.sync_challenge_runtime, challenge, enabled)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"Registry updated tapi runtime gagal: {e}")
|
||||||
|
return {"ok": True, "name": challenge, "enabled": enabled, "applied": report.get("teams", [])}
|
||||||
|
|
||||||
@app.get("/api/status")
|
@app.get("/api/status")
|
||||||
async def api_status(req: Request):
|
async def api_status(req: Request):
|
||||||
@@ -549,7 +585,7 @@ async def api_topology(req: Request):
|
|||||||
require_login(req)
|
require_login(req)
|
||||||
teams = orch.list_teams()
|
teams = orch.list_teams()
|
||||||
nodes = [
|
nodes = [
|
||||||
{"id": "panel", "label": "Panel A/D", "type": "panel", "url": "https://panel.gemastik.imrnes.team"},
|
{"id": "panel", "label": "Panel A/D", "type": "panel", "url": "https://panel.attackdefense.imrnes.team"},
|
||||||
{"id": "traefik", "label": "Traefik / Coolify", "type": "infra"},
|
{"id": "traefik", "label": "Traefik / Coolify", "type": "infra"},
|
||||||
{"id": "dns", "label": "*.imrnes.team → 43.134.105.109", "type": "infra"},
|
{"id": "dns", "label": "*.imrnes.team → 43.134.105.109", "type": "infra"},
|
||||||
]
|
]
|
||||||
@@ -687,7 +723,7 @@ async def api_admin_targets(req: Request):
|
|||||||
if not (d / "state.json").exists():
|
if not (d / "state.json").exists():
|
||||||
continue
|
continue
|
||||||
st = json.loads((d / "state.json").read_text())
|
st = json.loads((d / "state.json").read_text())
|
||||||
dom = st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".gemastik.imrnes.team")
|
dom = st.get("domain") or (st.get("slug", f"team{st.get('index')}") + ".attackdefense.imrnes.team")
|
||||||
for name, coff, soff in orch.CHALLENGES:
|
for name, coff, soff in orch.CHALLENGES:
|
||||||
p = st["ports"].get(name)
|
p = st["ports"].get(name)
|
||||||
if not p:
|
if not p:
|
||||||
|
|||||||
+68
-6
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Gemastik A/D Panel</title>
|
<title>Attack Defense Platform</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body {
|
body {
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>⚔️ GEMASTIK A/D — NODE CONTROL</h1>
|
<h1>⚔️ ATTACK DEFENSE PLATFORM — NODE CONTROL</h1>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<span class="pill" id="clock">--:--:--</span>
|
<span class="pill" id="clock">--:--:--</span>
|
||||||
<button onclick="refresh()">🔄 Refresh</button>
|
<button onclick="refresh()">🔄 Refresh</button>
|
||||||
@@ -135,6 +135,7 @@
|
|||||||
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</button>
|
<button class="tab" data-view="creds" onclick="showView('creds'); loadCreds()">🔑 Creds</button>
|
||||||
<button class="tab" data-view="targets" onclick="showView('targets'); loadTargetMatrix()">🎯 Target Matrix</button>
|
<button class="tab" data-view="targets" onclick="showView('targets'); loadTargetMatrix()">🎯 Target Matrix</button>
|
||||||
<button class="tab" data-view="sla" onclick="showView('sla'); loadSla()">📊 SLA & Skor</button>
|
<button class="tab" data-view="sla" onclick="showView('sla'); loadSla()">📊 SLA & Skor</button>
|
||||||
|
<button class="tab" data-view="chmgr" onclick="showView('chmgr'); loadChMgr()">🏗️ Challenge Manager</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Challenges view -->
|
<!-- Challenges view -->
|
||||||
@@ -229,7 +230,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-note">Receiver: https://gemastik.imrnes.team · Panel: https://panel.gemastik.imrnes.team · Auto-refresh tiap 15 detik</div>
|
<!-- Challenge Manager view -->
|
||||||
|
<div class="view" id="view-chmgr">
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||||
|
<b style="color:#5ad1ff">🏗️ Challenge Manager</b>
|
||||||
|
<span style="font-size:11px;color:#718096">Semua challenge dari registry (GEMASTIK 18, XVI, XVII). Toggle untuk aktif/nonaktif global di semua tim — diterapkan langsung (build/up atau stop/remove + restart receiver).</span>
|
||||||
|
<button class="primary" onclick="loadChMgr()" style="padding:4px 10px;font-size:12px">🔄 Muat ulang</button>
|
||||||
|
</div>
|
||||||
|
<div id="chMgrList" style="margin-top:14px;font-family:ui-monospace,monospace;font-size:12px;line-height:1.9;color:#dbe6f4;background:#0a101f;border:1px solid #1e3a5f;border-radius:10px;padding:14px;overflow-x:auto">Memuat…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-note">Receiver: https://attackdefense.imrnes.team · Panel: https://panel.attackdefense.imrnes.team · Auto-refresh tiap 15 detik</div>
|
||||||
|
|
||||||
<!-- modal flag -->
|
<!-- modal flag -->
|
||||||
<div class="modal-back" id="modalFlag">
|
<div class="modal-back" id="modalFlag">
|
||||||
@@ -310,6 +323,7 @@ function showView(v) {
|
|||||||
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
|
document.querySelectorAll('.view').forEach(x => x.classList.toggle('active', x.id === 'view-' + v));
|
||||||
if (v === 'topo') loadTopo(); // refresh attacks immediately on tab switch + every 10s
|
if (v === 'topo') loadTopo(); // refresh attacks immediately on tab switch + every 10s
|
||||||
if (v === 'sla') loadSla();
|
if (v === 'sla') loadSla();
|
||||||
|
if (v === 'chmgr') loadChMgr();
|
||||||
}
|
}
|
||||||
let topoTimer = null;
|
let topoTimer = null;
|
||||||
function startTopoTimer() {
|
function startTopoTimer() {
|
||||||
@@ -347,6 +361,54 @@ async function loadSla() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Challenge Manager ----------
|
||||||
|
async function loadChMgr() {
|
||||||
|
const box = document.getElementById('chMgrList');
|
||||||
|
if (!box) return;
|
||||||
|
box.innerHTML = 'Memuat…';
|
||||||
|
try {
|
||||||
|
const d = await api('/api/challenges');
|
||||||
|
const list = d.challenges || [];
|
||||||
|
const setLabels = { 'gemastik18': 'GEMASTIK 18', 'xvi': 'GEMASTIK XVI', 'xvii': 'GEMASTIK XVII' };
|
||||||
|
const bySet = {};
|
||||||
|
for (const c of list) (bySet[c.set] = bySet[c.set] || []).push(c);
|
||||||
|
let html = '';
|
||||||
|
for (const [set, chs] of Object.entries(bySet)) {
|
||||||
|
html += `<div style="margin:8px 0 4px;color:#5ad1ff;font-weight:700">📦 ${escapeHtml(setLabels[set] || set)} (${chs.length})</div>`;
|
||||||
|
for (const c of chs) {
|
||||||
|
const on = c.enabled ? 'checked' : '';
|
||||||
|
html += `<div style="display:flex;align-items:center;gap:10px;padding:6px 8px;border-bottom:1px solid #13233d;border-radius:6px">
|
||||||
|
<input type="checkbox" id="chmgr-${escapeHtml(c.name)}" ${on} onchange="toggleChallenge('${escapeHtml(c.name)}', this.checked)" style="width:16px;height:16px;accent-color:#22c55e">
|
||||||
|
<b style="min-width:150px">${escapeHtml(c.name)}</b>
|
||||||
|
<span style="color:#718096;font-size:11px">${escapeHtml(c.set)} · ${escapeHtml(c.category || '-')}</span>
|
||||||
|
<span style="color:#3d5a80;font-size:11px">chall+${c.chall_offset}/ssh+${c.ssh_offset}</span>
|
||||||
|
<span style="margin-left:auto;font-size:11px;color:${c.enabled ? '#22c55e' : '#e74c3c'}">${c.enabled ? '● AKTIF' : '○ NONAKTIF'}</span>
|
||||||
|
<span id="chmgr-msg-${escapeHtml(c.name)}" style="font-size:11px;color:#718096"></span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
box.innerHTML = html || '<div style="color:#718096">Tidak ada challenge di registry.</div>';
|
||||||
|
} catch (e) {
|
||||||
|
box.innerHTML = `<div style="color:#e74c3c">Gagal memuat: ${escapeHtml(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleChallenge(name, enabled) {
|
||||||
|
const msg = document.getElementById('chmgr-msg-' + name);
|
||||||
|
if (!msg) return;
|
||||||
|
msg.textContent = '⏳ menerapkan…';
|
||||||
|
msg.style.color = '#f5c542';
|
||||||
|
try {
|
||||||
|
const d = await api('/api/challenges/' + encodeURIComponent(name), { method: 'PATCH', body: JSON.stringify({ enabled }) });
|
||||||
|
msg.textContent = `✓ ${d.enabled ? 'diaktifkan' : 'dinonaktifkan'} (${(d.applied || []).length} tim)`;
|
||||||
|
msg.style.color = d.enabled ? '#22c55e' : '#e74c3c';
|
||||||
|
setTimeout(() => { msg.textContent = ''; loadChMgr(); }, 4000);
|
||||||
|
} catch (e) {
|
||||||
|
msg.textContent = '✗ ' + e.message;
|
||||||
|
msg.style.color = '#e74c3c';
|
||||||
|
loadChMgr();
|
||||||
|
}
|
||||||
|
}
|
||||||
// ---------- Challenges ----------
|
// ---------- Challenges ----------
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const grid = document.getElementById('grid');
|
const grid = document.getElementById('grid');
|
||||||
@@ -421,7 +483,7 @@ async function viewCred(ch) {
|
|||||||
const c = await api(`/api/credential/${ch}`);
|
const c = await api(`/api/credential/${ch}`);
|
||||||
document.getElementById('mcTitle').textContent = 'SSH Credentials — ' + ch;
|
document.getElementById('mcTitle').textContent = 'SSH Credentials — ' + ch;
|
||||||
const sshPort = {blogpost:10022, carbeat:11022, cdn:12022, phew:13022, sheesh:14022, warmup:15022}[ch] || 10022;
|
const sshPort = {blogpost:10022, carbeat:11022, cdn:12022, phew:13022, sheesh:14022, warmup:15022}[ch] || 10022;
|
||||||
const cmd = `ssh ctfuser@${ch}.gemastik.imrnes.team -p ${sshPort}`;
|
const cmd = `ssh ctfuser@${ch}.attackdefense.imrnes.team -p ${sshPort}`;
|
||||||
document.getElementById('mcBody').innerHTML =
|
document.getElementById('mcBody').innerHTML =
|
||||||
`<div>User: <b>ctfuser</b></div>
|
`<div>User: <b>ctfuser</b></div>
|
||||||
<div>Pass: <b>${esc(c.password)}</b></div>
|
<div>Pass: <b>${esc(c.password)}</b></div>
|
||||||
@@ -686,7 +748,7 @@ function teamCountChanged() {
|
|||||||
<span style="color:#718096;font-size:12px;width:60px">Team ${i}</span>
|
<span style="color:#718096;font-size:12px;width:60px">Team ${i}</span>
|
||||||
<input data-idx="${i}" data-field="name" placeholder="Nama team (contoh: Cyber Warriors)" style="flex:1;min-width:120px">
|
<input data-idx="${i}" data-field="name" placeholder="Nama team (contoh: Cyber Warriors)" style="flex:1;min-width:120px">
|
||||||
<input data-idx="${i}" data-field="domain" placeholder="domain custom (contoh: team-cyber)" style="flex:1;min-width:120px">
|
<input data-idx="${i}" data-field="domain" placeholder="domain custom (contoh: team-cyber)" style="flex:1;min-width:120px">
|
||||||
<span style="color:#3b4a63;font-size:11px">.gemastik.imrnes.team</span>
|
<span style="color:#3b4a63;font-size:11px">.attackdefense.imrnes.team</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
box.innerHTML = html;
|
box.innerHTML = html;
|
||||||
@@ -795,7 +857,7 @@ async function viewTeamCred(idx) {
|
|||||||
for (const [name, p] of Object.entries(t.team.ports)) {
|
for (const [name, p] of Object.entries(t.team.ports)) {
|
||||||
if (name === 'receiver' || name === 'panel') continue;
|
if (name === 'receiver' || name === 'panel') continue;
|
||||||
html += `<div class="kv" style="margin-top:6px">
|
html += `<div class="kv" style="margin-top:6px">
|
||||||
<b>${name}</b><span>ssh ctfuser@${name}.gemastik.imrnes.team -p ${p.ssh} · pass: ${esc(t.team.chall_passwords[name])}</span>
|
<b>${name}</b><span>ssh ctfuser@${name}.attackdefense.imrnes.team -p ${p.ssh} · pass: ${esc(t.team.chall_passwords[name])}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
document.getElementById('mtcTitle').textContent = `Kredensial Team ${idx}`;
|
document.getElementById('mtcTitle').textContent = `Kredensial Team ${idx}`;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Gemastik A/D Panel — Login</title>
|
<title>Attack Defense Platform — Login</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body {
|
body {
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>⚔️ GEMASTIK A/D</h1>
|
<h1>⚔️ ATTACK DEFENSE</h1>
|
||||||
<div class="sub">Node Control Panel — imrnes</div>
|
<div class="sub">Node Control Panel — imrnes</div>
|
||||||
<form id="f">
|
<form id="f">
|
||||||
<label>Username</label>
|
<label>Username</label>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<div class="logo">G</div>
|
<div class="logo">G</div>
|
||||||
<div>
|
<div>
|
||||||
<h1>Gemastik XVIII — Attack & Defense</h1>
|
<h1>Gemastik XVIII — Attack & Defense</h1>
|
||||||
<div class="sub">Submit flag untuk tim kamu. Server: gemastik.imrnes.team</div>
|
<div class="sub">Submit flag untuk tim kamu. Server: attackdefense.imrnes.team</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="team-id" content="0">
|
<meta name="team-id" content="0">
|
||||||
<title>Portal Tim — Gemastik A/D</title>
|
<title>Portal Tim — Attack Defense Platform</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
+151
-58
@@ -36,15 +36,133 @@ TEAMS_DIR = BASE / "teams"
|
|||||||
SERVICES_SRC = BASE / "services"
|
SERVICES_SRC = BASE / "services"
|
||||||
RECEIVER_SRC = BASE / "receiver"
|
RECEIVER_SRC = BASE / "receiver"
|
||||||
|
|
||||||
# Challenge definitions: (service name, chall port offset 0-5, ssh offset 22-27)
|
# ---------------------------------------------------------------------------
|
||||||
CHALLENGES = [
|
# Challenge registry — single source of truth across all distributed sets.
|
||||||
("blogpost", 0, 22),
|
# Loaded from teams/challenge_registry.json (admin can toggle enabled).
|
||||||
("carbeat", 1, 23),
|
#
|
||||||
("cdn", 2, 24),
|
# CHALLENGES below is a DERIVED list: (name, chall_offset, ssh_offset) for
|
||||||
("phew", 3, 25),
|
# every ENABLED challenge, in registry order. All team logic uses this.
|
||||||
("sheesh", 4, 26),
|
# ---------------------------------------------------------------------------
|
||||||
("warmup", 5, 27),
|
|
||||||
]
|
_REGISTRY_PATH = TEAMS_DIR / "challenge_registry.json"
|
||||||
|
|
||||||
|
def _default_registry() -> dict:
|
||||||
|
return {"sets": {}, "challenges": []}
|
||||||
|
|
||||||
|
def load_registry() -> dict:
|
||||||
|
try:
|
||||||
|
return json.loads(_REGISTRY_PATH.read_text())
|
||||||
|
except Exception:
|
||||||
|
return _default_registry()
|
||||||
|
|
||||||
|
def save_registry(reg: dict):
|
||||||
|
_REGISTRY_PATH.write_text(json.dumps(reg, indent=2))
|
||||||
|
|
||||||
|
def registry_challenges() -> list:
|
||||||
|
"""All challenge dicts from the registry (enabled or not), in order."""
|
||||||
|
return load_registry().get("challenges", [])
|
||||||
|
|
||||||
|
def enabled_challenges() -> list:
|
||||||
|
return [c for c in registry_challenges() if c.get("enabled")]
|
||||||
|
|
||||||
|
def set_challenge_enabled(name: str, enabled: bool) -> dict:
|
||||||
|
"""Flip a challenge's enabled flag in the registry (global toggle)."""
|
||||||
|
reg = load_registry()
|
||||||
|
for c in reg.get("challenges", []):
|
||||||
|
if c["name"] == name:
|
||||||
|
if bool(c.get("enabled")) == bool(enabled):
|
||||||
|
return {"unchanged": True, **c}
|
||||||
|
c["enabled"] = bool(enabled)
|
||||||
|
save_registry(reg)
|
||||||
|
return c
|
||||||
|
raise KeyError(f"Challenge {name} tidak ada di registry")
|
||||||
|
|
||||||
|
|
||||||
|
def sync_challenge_runtime(name: str, enabled: bool) -> dict:
|
||||||
|
"""Apply an enable/disable toggle to every RUNNING team:
|
||||||
|
- regenerate that team's compose from the registry (compose_gen)
|
||||||
|
- for ENABLE: docker compose up -d <name> (builds image first if needed)
|
||||||
|
- for DISABLE: docker compose rm -sf <name> (stop+remove the container)
|
||||||
|
- restart the team receiver so its challenge dict matches (main.py)
|
||||||
|
Returns a per-team report.
|
||||||
|
"""
|
||||||
|
import compose_gen
|
||||||
|
reg = load_registry()
|
||||||
|
ch = next((c for c in reg.get("challenges", []) if c["name"] == name), None)
|
||||||
|
if not ch:
|
||||||
|
raise KeyError(f"Challenge {name} tidak ada di registry")
|
||||||
|
# rebuild the derived CHALLENGES for fresh creates
|
||||||
|
global CHALLENGES
|
||||||
|
CHALLENGES = [(c["name"], c["chall_offset"], c["ssh_offset"]) for c in enabled_challenges()]
|
||||||
|
report = {"challenge": name, "enabled": bool(enabled), "teams": []}
|
||||||
|
for d in sorted(TEAMS_DIR.glob("team*")):
|
||||||
|
sf = d / "state.json"
|
||||||
|
if not sf.exists():
|
||||||
|
continue
|
||||||
|
st = json.loads(sf.read_text())
|
||||||
|
idx = st["index"]
|
||||||
|
svc_dir = d / "services"
|
||||||
|
try:
|
||||||
|
if enabled:
|
||||||
|
# fresh flag file for this challenge
|
||||||
|
flags_dir = d / "receiver" / "flags"
|
||||||
|
flags_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
flag = f"GEMASTIK18{{TEAM{idx}_{name.upper()}_{secrets.token_hex(6)}}}"
|
||||||
|
(flags_dir / f"{name}.txt").write_text(flag)
|
||||||
|
st.setdefault("flags", {})[name] = flag
|
||||||
|
st["ports"][name] = {"chall": 30000 + idx*1000 + ch["chall_offset"],
|
||||||
|
"ssh": 30000 + idx*1000 + ch["ssh_offset"]}
|
||||||
|
st.setdefault("chall_passwords", {})[name] = st["chall_passwords"].get(
|
||||||
|
name) or f"chall{idx}_{name}_{secrets.token_hex(4)}"
|
||||||
|
# write state BEFORE rendering (render needs ports[name])
|
||||||
|
(sf).write_text(json.dumps(st, indent=2))
|
||||||
|
# regenerate whole compose (so enabled challenge included),
|
||||||
|
# then bring up just this service
|
||||||
|
new_text = compose_gen.render_team_compose(idx, st)
|
||||||
|
(svc_dir / "docker-compose.yml").write_text(new_text)
|
||||||
|
# inject the team-specific password env if compose uses ${PASSWORD_*}
|
||||||
|
envp = svc_dir / ".env"
|
||||||
|
if not envp.exists():
|
||||||
|
env_lines = [f"ADMIN_USERNAME={st['admin_user']}", f"ADMIN_PASSWORD={st['admin_pass']}",
|
||||||
|
f"COMPOSE_LOCATION={svc_dir}/docker-compose.yml"]
|
||||||
|
for i in range(20):
|
||||||
|
env_lines.append(f"PASSWORD_{(i*1000)+10000}=placeholder")
|
||||||
|
(envp).write_text("\n".join(env_lines) + "\n")
|
||||||
|
r = subprocess.run(["docker", "compose", "-p", f"team{idx}", "-f", svc_dir / "docker-compose.yml",
|
||||||
|
"up", "-d", "--build", name],
|
||||||
|
cwd=str(svc_dir), capture_output=True, text=True, timeout=1800)
|
||||||
|
ok = r.returncode == 0
|
||||||
|
report["teams"].append({"team": idx, "ok": ok, "detail": (r.stdout or r.stderr)[-300:]})
|
||||||
|
if ok:
|
||||||
|
# set the per-team SSH password after container boot
|
||||||
|
try:
|
||||||
|
pw = st["chall_passwords"][name]
|
||||||
|
subprocess.run(["docker", "exec", f"{name}_container_team{idx}", "sh", "-c",
|
||||||
|
f"echo 'ctfuser:{pw}' | chpasswd"], capture_output=True, timeout=60)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# stop + remove the container FIRST (old compose), then regenerate
|
||||||
|
r = subprocess.run(["docker", "compose", "-p", f"team{idx}", "-f", svc_dir / "docker-compose.yml",
|
||||||
|
"rm", "-sf", name],
|
||||||
|
cwd=str(svc_dir), capture_output=True, text=True, timeout=120)
|
||||||
|
# remove from state ports/flags
|
||||||
|
st["ports"].pop(name, None)
|
||||||
|
st.setdefault("flags", {}).pop(name, None)
|
||||||
|
(sf).write_text(json.dumps(st, indent=2))
|
||||||
|
# regenerate compose WITHOUT this challenge
|
||||||
|
new_text = compose_gen.render_team_compose(idx, st)
|
||||||
|
(svc_dir / "docker-compose.yml").write_text(new_text)
|
||||||
|
report["teams"].append({"team": idx, "ok": True,
|
||||||
|
"detail": (r.stdout or r.stderr)[-300:] or "removed"})
|
||||||
|
# restart receiver so its challenge set matches
|
||||||
|
_start_receiver(idx)
|
||||||
|
except Exception as e:
|
||||||
|
report["teams"].append({"team": idx, "ok": False, "detail": str(e)[-300:]})
|
||||||
|
return report
|
||||||
|
|
||||||
|
# Derived list used everywhere (keeps old API: tuples of name, coff, soff)
|
||||||
|
CHALLENGES = [(c["name"], c["chall_offset"], c["ssh_offset"]) for c in enabled_challenges()]
|
||||||
|
|
||||||
# Points system: base points earned by stealing a flag from another team's
|
# Points system: base points earned by stealing a flag from another team's
|
||||||
# challenge. The SLA bonus is earned by keeping your OWN services alive.
|
# challenge. The SLA bonus is earned by keeping your OWN services alive.
|
||||||
@@ -96,7 +214,8 @@ def add_sla_bonus(team_idx: int, alive: int, total: int = 6) -> dict:
|
|||||||
me = data["teams"].setdefault(tid, {"points": 0, "events": []})
|
me = data["teams"].setdefault(tid, {"points": 0, "events": []})
|
||||||
now = time.time()
|
now = time.time()
|
||||||
last = me.get("last_sla_bonus", 0)
|
last = me.get("last_sla_bonus", 0)
|
||||||
if alive >= SLA_BONUS_MIN_ALIVE and total >= SLA_BONUS_MIN_ALIVE:
|
min_alive = max(1, len(enabled_challenges()))
|
||||||
|
if alive >= min_alive and total >= min_alive:
|
||||||
if now - last > 300: # 5 min window
|
if now - last > 300: # 5 min window
|
||||||
me["points"] = int(me.get("points", 0)) + SLA_BONUS_POINTS
|
me["points"] = int(me.get("points", 0)) + SLA_BONUS_POINTS
|
||||||
me["last_sla_bonus"] = now
|
me["last_sla_bonus"] = now
|
||||||
@@ -155,18 +274,18 @@ def create_team(idx: int, label: str = None, domain: str = None):
|
|||||||
|
|
||||||
label -> team display name (leaderboard/topology)
|
label -> team display name (leaderboard/topology)
|
||||||
domain -> custom subdomain host, e.g. "cyber-warriors" or
|
domain -> custom subdomain host, e.g. "cyber-warriors" or
|
||||||
"cyber-warriors.gemastik.imrnes.team" (full host accepted).
|
"cyber-warriors.attackdefense.imrnes.team" (full host accepted).
|
||||||
Defaults to slugify(label).gemastik.imrnes.team.
|
Defaults to slugify(label).attackdefense.imrnes.team.
|
||||||
"""
|
"""
|
||||||
ports = team_ports(idx)
|
ports = team_ports(idx)
|
||||||
team_dir = TEAMS_DIR / f"team{idx}"
|
team_dir = TEAMS_DIR / f"team{idx}"
|
||||||
label = label or f"Tim {idx}"
|
label = label or f"Tim {idx}"
|
||||||
slug = slugify(label)
|
slug = slugify(label)
|
||||||
# normalize custom domain -> host under *.gemastik.imrnes.team
|
# normalize custom domain -> host under *.attackdefense.imrnes.team
|
||||||
host = (domain or f"{slug}.gemastik.imrnes.team").strip().lower()
|
host = (domain or f"{slug}.attackdefense.imrnes.team").strip().lower()
|
||||||
host = host.replace("https://", "").replace("http://", "").rstrip("/")
|
host = host.replace("https://", "").replace("http://", "").rstrip("/")
|
||||||
if not host.endswith(".gemastik.imrnes.team"):
|
if not host.endswith(".attackdefense.imrnes.team"):
|
||||||
host = f"{host}.gemastik.imrnes.team"
|
host = f"{host}.attackdefense.imrnes.team"
|
||||||
slug = host.split(".")[0]
|
slug = host.split(".")[0]
|
||||||
state = {
|
state = {
|
||||||
"index": idx,
|
"index": idx,
|
||||||
@@ -188,8 +307,8 @@ def create_team(idx: int, label: str = None, domain: str = None):
|
|||||||
svc_dir = team_dir / "services"
|
svc_dir = team_dir / "services"
|
||||||
if svc_dir.exists():
|
if svc_dir.exists():
|
||||||
shutil.rmtree(svc_dir)
|
shutil.rmtree(svc_dir)
|
||||||
shutil.copytree(SERVICES_SRC, svc_dir, ignore=shutil.ignore_patterns("__pycache__", ".git", "exploits", "exploit"))
|
svc_dir.mkdir(parents=True, exist_ok=True)
|
||||||
# compose references ../utils/bashrc etc — copy utils next to services
|
# copy utils next to services (compose references ../utils/bashrc)
|
||||||
utils_src = BASE / "utils"
|
utils_src = BASE / "utils"
|
||||||
utils_dst = team_dir / "utils"
|
utils_dst = team_dir / "utils"
|
||||||
if utils_src.exists():
|
if utils_src.exists():
|
||||||
@@ -202,45 +321,19 @@ def create_team(idx: int, label: str = None, domain: str = None):
|
|||||||
if bashrc.exists():
|
if bashrc.exists():
|
||||||
bashrc.write_text(bashrc.read_text().replace(
|
bashrc.write_text(bashrc.read_text().replace(
|
||||||
"host.docker.internal:18080", f"host.docker.internal:{recv_port}"))
|
"host.docker.internal:18080", f"host.docker.internal:{recv_port}"))
|
||||||
|
# generate compose from the challenge registry (compose_gen renders the
|
||||||
|
# per-team file: image: services-<name>, team ports, team passwords)
|
||||||
|
import compose_gen
|
||||||
|
compose_text = compose_gen.render_team_compose(idx, state)
|
||||||
compose = svc_dir / "docker-compose.yml"
|
compose = svc_dir / "docker-compose.yml"
|
||||||
text = compose.read_text()
|
compose.write_text(compose_text)
|
||||||
# --- replace build: blocks with image: so teams reuse the base images (no rebuild) ---
|
|
||||||
# Each service's build block looks like:
|
|
||||||
# build:
|
|
||||||
# context: <name>
|
|
||||||
# args:
|
|
||||||
# - PASSWORD=$PASSWORD_XXXXX
|
|
||||||
# Replace the whole block with " image: services-<name>".
|
|
||||||
for name, coff, soff in CHALLENGES:
|
|
||||||
text = re.sub(
|
|
||||||
rf" build:\n context: {name}\n args:\n - PASSWORD=\$PASSWORD_[0-9]+\n",
|
|
||||||
f" image: services-{name}\n",
|
|
||||||
text)
|
|
||||||
compose.write_text(text)
|
|
||||||
# rewrite container names + ports per challenge
|
|
||||||
for name, coff, soff in CHALLENGES:
|
|
||||||
cont_old = f"{name}_container"
|
|
||||||
cont_new = f"{name}_container_team{idx}"
|
|
||||||
text = text.replace(f"container_name: {cont_old}", f"container_name: {cont_new}")
|
|
||||||
text = text.replace(f"hostname: {name}", f"hostname: {name}_team{idx}")
|
|
||||||
# ports mapping: "10000:8000" -> "<team_chall>:8000"
|
|
||||||
old_chall = str(10000 + coff * 1000) # 10000,11000,12000,13000,14000,15000
|
|
||||||
old_ssh = str(10022 + coff * 1000) # 10022,11022,...
|
|
||||||
text = re.sub(rf'"({old_chall}):', f'"{ports[name]["chall"]}:', text)
|
|
||||||
text = re.sub(rf'"({old_ssh}):', f'"{ports[name]["ssh"]}:', text)
|
|
||||||
# PASSWORD_* args -> team passwords
|
|
||||||
for name, coff, soff in CHALLENGES:
|
|
||||||
old_env = f"PASSWORD_{10000 + coff * 1000}"
|
|
||||||
text = re.sub(rf"\${{{old_env}}}", state["chall_passwords"][name], text)
|
|
||||||
# compose file references services/.env — we'll create it below
|
|
||||||
compose.write_text(text)
|
|
||||||
|
|
||||||
# team services/.env (PASSWORD_* in same shape as starter.py)
|
# team services/.env (PASSWORD_* in same shape as starter.py)
|
||||||
env_lines = [f"ADMIN_USERNAME={state['admin_user']}", f"ADMIN_PASSWORD={state['admin_pass']}"]
|
env_lines = [f"ADMIN_USERNAME={state['admin_user']}", f"ADMIN_PASSWORD={state['admin_pass']}"]
|
||||||
env_lines.append(f"COMPOSE_LOCATION={svc_dir}/docker-compose.yml")
|
env_lines.append(f"COMPOSE_LOCATION={svc_dir}/docker-compose.yml")
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
env_lines.append(f"PASSWORD_{(i*1000)+10000}={gen_password(20)}")
|
env_lines.append(f"PASSWORD_{(i*1000)+10000}={gen_password(20)}")
|
||||||
# force the six used passwords to team ones
|
# force the used passwords to team ones
|
||||||
for name, coff, soff in CHALLENGES:
|
for name, coff, soff in CHALLENGES:
|
||||||
for j, line in enumerate(env_lines):
|
for j, line in enumerate(env_lines):
|
||||||
if line.startswith(f"PASSWORD_{10000+coff*1000}="):
|
if line.startswith(f"PASSWORD_{10000+coff*1000}="):
|
||||||
@@ -294,8 +387,8 @@ def update_team(idx: int, label: str = None, domain: str = None) -> dict:
|
|||||||
st["label"] = str(label).strip()[:48] or st["label"]
|
st["label"] = str(label).strip()[:48] or st["label"]
|
||||||
if domain:
|
if domain:
|
||||||
host = domain.strip().lower().replace("https://", "").replace("http://", "").rstrip("/")
|
host = domain.strip().lower().replace("https://", "").replace("http://", "").rstrip("/")
|
||||||
if not host.endswith(".gemastik.imrnes.team"):
|
if not host.endswith(".attackdefense.imrnes.team"):
|
||||||
host = f"{host}.gemastik.imrnes.team"
|
host = f"{host}.attackdefense.imrnes.team"
|
||||||
st["domain"] = host
|
st["domain"] = host
|
||||||
st["slug"] = host.split(".")[0]
|
st["slug"] = host.split(".")[0]
|
||||||
(team_dir / "state.json").write_text(json.dumps(st, indent=2))
|
(team_dir / "state.json").write_text(json.dumps(st, indent=2))
|
||||||
@@ -399,7 +492,7 @@ def team_logs(idx: int, service: str = None, tail: int = 100):
|
|||||||
def ensure_team_domains() -> str:
|
def ensure_team_domains() -> str:
|
||||||
"""Write Traefik dynamic config for each team domain -> panel (:18081).
|
"""Write Traefik dynamic config for each team domain -> panel (:18081).
|
||||||
|
|
||||||
Each team gets <slug>.gemastik.imrnes.team. The panel routes
|
Each team gets <slug>.attackdefense.imrnes.team. The panel routes
|
||||||
/team/<idx> to that team's portal page (public, no panitia login),
|
/team/<idx> to that team's portal page (public, no panitia login),
|
||||||
and /api/team/<idx>/* serves team-scoped API. Writing this into the
|
and /api/team/<idx>/* serves team-scoped API. Writing this into the
|
||||||
same dynamic dir Traefik watches means domains appear automatically.
|
same dynamic dir Traefik watches means domains appear automatically.
|
||||||
@@ -419,10 +512,10 @@ def ensure_team_domains() -> str:
|
|||||||
td = TEAMS_DIR / f"team{t['index']}"
|
td = TEAMS_DIR / f"team{t['index']}"
|
||||||
st = json.loads((td / "state.json").read_text())
|
st = json.loads((td / "state.json").read_text())
|
||||||
st["slug"] = slug
|
st["slug"] = slug
|
||||||
st["domain"] = f"{slug}.gemastik.imrnes.team"
|
st["domain"] = f"{slug}.attackdefense.imrnes.team"
|
||||||
(td / "state.json").write_text(json.dumps(st, indent=2))
|
(td / "state.json").write_text(json.dumps(st, indent=2))
|
||||||
changed = True
|
changed = True
|
||||||
host = f"{slug}.gemastik.imrnes.team"
|
host = f"{slug}.attackdefense.imrnes.team"
|
||||||
out.append(f""" team-{slug}-http:
|
out.append(f""" team-{slug}-http:
|
||||||
rule: Host(`{host}`)
|
rule: Host(`{host}`)
|
||||||
entryPoints:
|
entryPoints:
|
||||||
@@ -442,9 +535,9 @@ def ensure_team_domains() -> str:
|
|||||||
""")
|
""")
|
||||||
if not out:
|
if not out:
|
||||||
return "no teams to route"
|
return "no teams to route"
|
||||||
# Keep the team domain block in its own file so the main gemastik.yaml stays untouched
|
# Keep the team domain block in its own file so the main attackdefense.yaml stays untouched
|
||||||
(traefik_dir / "gemastik-teams.yaml").write_text("http:\n routers:\n" + "".join(out))
|
(traefik_dir / "attackdefense-teams.yaml").write_text("http:\n routers:\n" + "".join(out))
|
||||||
return f"wrote {len(out)} team domain(s) in gemastik-teams.yaml"
|
return f"wrote {len(out)} team domain(s) in attackdefense-teams.yaml"
|
||||||
|
|
||||||
|
|
||||||
def list_teams() -> list:
|
def list_teams() -> list:
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ class Challenge(object):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def credentials(self):
|
def credentials(self):
|
||||||
|
pwd = os.environ.get(f'PASSWORD_{self.port}')
|
||||||
|
if not pwd:
|
||||||
|
pwd = getattr(self.settings, f'PASSWORD_{self.port}', '')
|
||||||
return {
|
return {
|
||||||
'username': 'ctfuser',
|
'username': 'ctfuser',
|
||||||
'password': getattr(self.settings, f'PASSWORD_{self.port}'),
|
'password': pwd,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import io
|
||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
|
||||||
|
class Art(Challenge):
|
||||||
|
flag_location = 'flags/art.txt'
|
||||||
|
history_location = 'history/art.txt'
|
||||||
|
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
word = self.random_string(8)
|
||||||
|
url = f'http://localhost:{self.port}/art/{word}'
|
||||||
|
r = requests.get(url, timeout=5)
|
||||||
|
assert r.text == f'<iframe height="100%" width="100%" frameborder="0" src=https://asciified.thelicato.io/api/v2/ascii?text={word}></iframe>', 'Unexpected response'
|
||||||
|
self.logger.info('Check passed for art')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check art: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
from pwn import *
|
||||||
|
|
||||||
|
class BackToBasic(Challenge):
|
||||||
|
flag_location = 'flags/back-to-basic.txt'
|
||||||
|
history_location = 'history/back-to-basic.txt'
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
r = remote("localhost",self.port)
|
||||||
|
assert b"idea?" in r.recvline(), "Failed First"
|
||||||
|
|
||||||
|
r.sendline(b"testt")
|
||||||
|
|
||||||
|
assert b"thing" in r.recvline(), "Failed Last"
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check back-to-basic: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
from fastecdsa.curve import Curve
|
||||||
|
from fastecdsa.point import Point
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class Burvesigner(Challenge):
|
||||||
|
flag_location = 'flags/burvesigner.txt'
|
||||||
|
history_location = 'history/burvesigner.txt'
|
||||||
|
priv_location = 'files/burvesigner.priv'
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.priv_location, 'wb') as f:
|
||||||
|
f.write(os.urandom(256))
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f'http://localhost:{self.port}'
|
||||||
|
flag = open(self.flag_location).read()
|
||||||
|
|
||||||
|
# C1: login guest success
|
||||||
|
guest_data = {
|
||||||
|
'username': 'guest',
|
||||||
|
'password': 'guest',
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=guest_data, timeout=5)
|
||||||
|
guest_token = response.cookies["token"]
|
||||||
|
assert "Welcome, guest!" in response.text, "Guest cannot login"
|
||||||
|
assert "flashes" not in response.text, "Guest cannot login"
|
||||||
|
self.logger.info(f'C1 success for burvesigner')
|
||||||
|
|
||||||
|
# C2: login admin success
|
||||||
|
admin_data = {
|
||||||
|
'username': 'merricx_number_1_fans',
|
||||||
|
'password': getattr(self.settings, f'PASSWORD_{self.port}'),
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=admin_data, timeout=5)
|
||||||
|
admin_token = response.cookies["token"]
|
||||||
|
assert "Welcome, merricx_number_1_fans!" in response.text, "Admin cannot login"
|
||||||
|
assert flag in response.text, "Flag is missing in admin page"
|
||||||
|
assert "flashes" not in response.text, "Admin cannot login"
|
||||||
|
self.logger.info(f'C2 success for burvesigner')
|
||||||
|
|
||||||
|
# C3: login guest fail
|
||||||
|
guest_data = {
|
||||||
|
'username': 'guest',
|
||||||
|
'password': 'Guest',
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=guest_data, timeout=5).text
|
||||||
|
assert "Welcome, guest!" not in response, "Guest with wrong credential can login"
|
||||||
|
assert "flashes" in response, "Guest with wrong credential can login"
|
||||||
|
self.logger.info(f'C3 success for burvesigner')
|
||||||
|
|
||||||
|
# C4: login admin fail
|
||||||
|
admin_data = {
|
||||||
|
'username': 'merricx_number_1_fans',
|
||||||
|
'password': 'password',
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=admin_data, timeout=5).text
|
||||||
|
assert "Welcome, merricx_number_1_fans!" not in response, "Admin with wrong credential can login"
|
||||||
|
assert flag not in response, "Flag is showing in admin page with wrong credential"
|
||||||
|
assert "flashes" in response, "Admin with wrong credential can login"
|
||||||
|
self.logger.info(f'C4 success for burvesigner')
|
||||||
|
|
||||||
|
self.logger.info('calling sleep(5)')
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# C5: cek apakah token guest di C1 sudah expired atau belum
|
||||||
|
response = requests.get(url, cookies={"token": guest_token}, timeout=5)
|
||||||
|
assert "Welcome, guest!" in response.text, "Guest token expires early"
|
||||||
|
assert "flashes" not in response.text, "Guest token expires early"
|
||||||
|
self.logger.info(f'C5 success for burvesigner')
|
||||||
|
|
||||||
|
# C6: cek apakah token admin di C2 sudah expired atau belum
|
||||||
|
response = requests.get(url, cookies={"token": admin_token}, timeout=5)
|
||||||
|
assert "Welcome, merricx_number_1_fans!" in response.text, "Admin token expires early"
|
||||||
|
assert flag in response.text, "Admin token expires early"
|
||||||
|
assert "flashes" not in response.text, "Admin token expires early"
|
||||||
|
self.logger.info(f'C6 success for burvesigner')
|
||||||
|
|
||||||
|
# C7: cek endpoint /params
|
||||||
|
response = requests.get(url + "/params", timeout=5).text
|
||||||
|
response = response.replace("<pre>", "").replace("</pre>", "")
|
||||||
|
params = json.loads(response)
|
||||||
|
assert params["p"] and params["a"] and params["b"] and params["n"], "Missing p, a, b and/or n parameter(s)"
|
||||||
|
assert params["G"][0] and params["G"][1] and params["Y"][0] and params["Y"][1], "Missing G and/or Y point(s)"
|
||||||
|
self.logger.info(f'C7 success for burvesigner')
|
||||||
|
|
||||||
|
# C8: cek apakah curve C valid dan point G di C
|
||||||
|
C = Curve("burvesigner", params["p"], params["a"], params["b"], params["n"], params["G"][0], params["G"][1])
|
||||||
|
assert C.G == Point(params["G"][0], params["G"][1], C), "Point G is not valid"
|
||||||
|
self.logger.info(f'C8 success for burvesigner')
|
||||||
|
|
||||||
|
# C9: cek apakah point G * priv = Y
|
||||||
|
t = params["p"].bit_length() // 8
|
||||||
|
priv = open(self.priv_location, "rb").read()[:t]
|
||||||
|
x = int.from_bytes(priv, "little")
|
||||||
|
Y = Point(params["Y"][0], params["Y"][1], C)
|
||||||
|
assert C.G * x == Y, "Point Y is not valid"
|
||||||
|
self.logger.info(f'C9 success for burvesigner')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check burvesigner: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Challenge(object):
|
||||||
|
name = __name__
|
||||||
|
settings = get_settings()
|
||||||
|
port = 0
|
||||||
|
|
||||||
|
def __init__(self, port):
|
||||||
|
self.port = port
|
||||||
|
self.add_logger()
|
||||||
|
|
||||||
|
def add_logger(self):
|
||||||
|
self.logger = logging.getLogger()
|
||||||
|
|
||||||
|
def random_string(self, length):
|
||||||
|
charset = string.ascii_uppercase + string.ascii_lowercase + string.digits
|
||||||
|
return ''.join(random.choice(charset) for i in range(length))
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def credentials(self):
|
||||||
|
pwd = os.environ.get(f'PASSWORD_{self.port}')
|
||||||
|
if not pwd:
|
||||||
|
pwd = getattr(self.settings, f'PASSWORD_{self.port}', '')
|
||||||
|
return {
|
||||||
|
'username': 'root',
|
||||||
|
'password': pwd,
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
MOCK_URL = 'http://google.com'
|
||||||
|
MOCK_DATA = '<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">'
|
||||||
|
|
||||||
|
class Crawlback(Challenge):
|
||||||
|
flag_location = 'flags/crawlback.txt'
|
||||||
|
history_location = 'history/crawlback.txt'
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
r = requests.post(f"http://localhost:{self.port}/crawlback.php", data={'url': MOCK_URL})
|
||||||
|
|
||||||
|
assert r.text.split('\n').pop(0) == MOCK_DATA
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check crawlback: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import zlib
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
|
||||||
|
MOCK_URL = 'http://google.com'
|
||||||
|
MOCK_DATA_WGET = 'Google</title>'
|
||||||
|
MOCK_DATA_CURL = '<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">'
|
||||||
|
|
||||||
|
class GemasFetcher(Challenge):
|
||||||
|
flag_location = 'flags/gemas-fetcher.txt'
|
||||||
|
history_location = 'history/gemas-fetcher.txt'
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
sess = requests.Session()
|
||||||
|
|
||||||
|
## register
|
||||||
|
username = self.random_string(5)
|
||||||
|
password = self.random_string(5)
|
||||||
|
r = sess.post(f"http://localhost:{self.port}/auth/register", data={"username":username,"password": password}, allow_redirects=False)
|
||||||
|
assert r.headers.get("location") == "/auth/login", "Register Failed"
|
||||||
|
|
||||||
|
## login
|
||||||
|
r = sess.post(f"http://localhost:{self.port}/auth/login", data={"username":username,"password": password}, allow_redirects=False)
|
||||||
|
assert r.headers.get("location") == "/dashboard", "Login Failed"
|
||||||
|
|
||||||
|
## wget
|
||||||
|
content = {"provider": "wget","url":MOCK_URL}
|
||||||
|
files = {"file": ("visit", b"\x00\x00"+gzip.compress(zlib.compress(json.dumps(content).encode())))}
|
||||||
|
r = sess.post(f"http://localhost:{self.port}/dashboard/fetch_by_file", files=files)
|
||||||
|
assert MOCK_DATA_WGET in r.text, "wget Failed"
|
||||||
|
|
||||||
|
## curl
|
||||||
|
content = {"provider": "curl","url":MOCK_URL}
|
||||||
|
files = {"file": ("visit", b"\x00\x01"+gzip.compress(zlib.compress(json.dumps(content).encode())))}
|
||||||
|
r = sess.post(f"http://localhost:{self.port}/dashboard/fetch_by_file", files=files)
|
||||||
|
assert r.text.split('\n').pop(0) == MOCK_DATA_CURL, "curl Failed"
|
||||||
|
|
||||||
|
## python
|
||||||
|
content = {"provider": "python","url":MOCK_URL}
|
||||||
|
files = {"file": ("visit", b"\x00\x02"+gzip.compress(zlib.compress(json.dumps(content).encode())))}
|
||||||
|
r = sess.post(f"http://localhost:{self.port}/dashboard/fetch_by_file", files=files)
|
||||||
|
assert r.text.startswith('"PCFkb2N0eXBlIGh0bWw'), "python Failed"
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check gemas-fetcher: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
class GemasNotes(Challenge):
|
||||||
|
history_location = 'history/gemas-notes.txt'
|
||||||
|
host = "http://localhost:12000"
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
username = "gemasflagreceiver"
|
||||||
|
password = "AuTeEbn%.Q5$pC_ge6"
|
||||||
|
result = requests.post(f"{self.host}/flag_receiver", json={"flag": flag}, auth=(username,password)).json()
|
||||||
|
if not result.get("success"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} updated in gemas-notes database')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could send flag to gemas-notes challenge: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f'http://localhost:{self.port}'
|
||||||
|
|
||||||
|
# login
|
||||||
|
token = requests.post(f"{url}/api/login",json={"email":"checker@gemasnotes.id", "password":"uRIqCvJ<IGb;VDT14"}).json()["token"]
|
||||||
|
header = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
# get count
|
||||||
|
old_count = requests.post(f"{url}/api/notes/count", headers=header, json={"count_by":"title", "keyword":""}).json()["count"]
|
||||||
|
|
||||||
|
# create notes
|
||||||
|
notes = {"title":self.random_string(10), "content":self.random_string(20), "tags":self.random_string(10)}
|
||||||
|
status_code = requests.put(f"{url}/api/notes", headers=header, json=notes).status_code
|
||||||
|
assert status_code in [200, 201], "Cannot Create Note"
|
||||||
|
|
||||||
|
# get notes
|
||||||
|
all_notes = requests.get(f"{url}/api/notes").json()
|
||||||
|
note = list(filter(lambda x: x["title"] == notes["title"], all_notes))
|
||||||
|
assert len(note) != 0, "Note was not created"
|
||||||
|
|
||||||
|
# get new count
|
||||||
|
new_count = requests.post(f"{url}/api/notes/count", headers=header, json={"count_by":"title", "keyword":""}).json()["count"]
|
||||||
|
assert old_count != new_count, "Invalid count"
|
||||||
|
|
||||||
|
# update notes
|
||||||
|
new_content = self.random_string(20)
|
||||||
|
notes["id"] = note[0]["id"]
|
||||||
|
notes["content"] = new_content
|
||||||
|
status_code = requests.patch(f"{url}/api/notes", headers=header, json=notes).status_code
|
||||||
|
assert status_code in [200, 204], "Cannot Update Note"
|
||||||
|
|
||||||
|
# delete notes
|
||||||
|
status_code = requests.delete(f"{url}/api/notes/{notes['id']}", headers=header, json=notes).status_code
|
||||||
|
assert status_code == 200, "Cannot Delete Note"
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check gemas-notes: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import requests
|
||||||
|
from base64 import b64decode
|
||||||
|
|
||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
|
||||||
|
class Hirnfick(Challenge):
|
||||||
|
flag_location = 'flags/hirnfick.txt'
|
||||||
|
history_location = 'history/hirnfick.txt'
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
res = requests.post(
|
||||||
|
f"http://localhost:{self.port}/api/run",
|
||||||
|
timeout=5,
|
||||||
|
json={
|
||||||
|
"code":
|
||||||
|
"+[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+."
|
||||||
|
})
|
||||||
|
|
||||||
|
assert b64decode(res.json()["output"]) == b"HirnFick 1.0\nHello, World!"
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check hirnfick: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class Pasta(Challenge):
|
||||||
|
flag_location = 'flags/pasta.txt'
|
||||||
|
history_location = 'history/pasta.txt'
|
||||||
|
host = "http://localhost:13000"
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f'http://localhost:{self.port}'
|
||||||
|
username = f"checker-{self.random_string(8)}"
|
||||||
|
pwd = self.random_string(12)
|
||||||
|
flag = open(self.flag_location).read()
|
||||||
|
|
||||||
|
admin_data = {
|
||||||
|
'username': 'deomkicer_number_1_fans',
|
||||||
|
'password': getattr(self.settings, f'PASSWORD_{self.port}'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# login admin and check flag
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/auth",
|
||||||
|
json=admin_data).json()
|
||||||
|
|
||||||
|
token = response.get('token')
|
||||||
|
assert token, "Token is missing in login admin"
|
||||||
|
check_flag = requests.get(f"{url}/flag", headers={'Authorization': f"Bearer {token}"}).json()
|
||||||
|
assert check_flag.get('flag') == flag, "Flag is missing/mismatch"
|
||||||
|
|
||||||
|
# register
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/register",
|
||||||
|
json={
|
||||||
|
"username": f"{username}",
|
||||||
|
"password": f"{pwd}"}).json()
|
||||||
|
|
||||||
|
assert response.get('success') == "User registered succesfully", "Register failed"
|
||||||
|
|
||||||
|
# login with version 1
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/auth?version=1",
|
||||||
|
json={
|
||||||
|
"username": f"{username}",
|
||||||
|
"password": f"{pwd}"}).json()
|
||||||
|
|
||||||
|
token = response.get('token')
|
||||||
|
assert token, "Token is missing in login v1"
|
||||||
|
check_home = requests.get(f"{url}/", headers={'Authorization': f"Bearer {token}"}).json()
|
||||||
|
assert check_home.get('username') == username, "Different username found in login v1"
|
||||||
|
|
||||||
|
# login with version 2
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/auth?version=2",
|
||||||
|
json={
|
||||||
|
"username": f"{username}",
|
||||||
|
"password": f"{pwd}"}).json()
|
||||||
|
|
||||||
|
token = response.get('token')
|
||||||
|
assert token, "Token is missing in login v2"
|
||||||
|
check_home = requests.get(f"{url}/", headers={'Authorization': f"Bearer {token}"}).json()
|
||||||
|
assert check_home.get('username') == username, "Different username found in login v2"
|
||||||
|
|
||||||
|
# login with version 3
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/auth?version=3",
|
||||||
|
json={
|
||||||
|
"username": f"{username}",
|
||||||
|
"password": f"{pwd}"}).json()
|
||||||
|
|
||||||
|
token = response.get('token')
|
||||||
|
assert token, "Token is missing in login v3"
|
||||||
|
check_home = requests.get(f"{url}/", headers={'Authorization': f"Bearer {token}"}).json()
|
||||||
|
assert check_home.get('username') == username, "Different username found in login v3"
|
||||||
|
|
||||||
|
# login with version 4
|
||||||
|
response = requests.post(
|
||||||
|
f"{url}/auth?version=4",
|
||||||
|
json={
|
||||||
|
"username": f"{username}",
|
||||||
|
"password": f"{pwd}"}).json()
|
||||||
|
|
||||||
|
token = response.get('token')
|
||||||
|
assert token, "Token is missing in login v4"
|
||||||
|
check_home = requests.get(f"{url}/", headers={'Authorization': f"Bearer {token}"}).json()
|
||||||
|
assert check_home.get('username') == username, "Different username found in login v4"
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check pasta: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class S3(Challenge):
|
||||||
|
flag_location = 'flags/s3.txt'
|
||||||
|
history_location = 'history/s3.txt'
|
||||||
|
host = 'http://localhost:20000'
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
filename = self.random_string(8) + ".txt"
|
||||||
|
content = self.random_string(64)
|
||||||
|
|
||||||
|
r = requests.post(f"http://localhost:{self.port}/upload", files={'file': (filename, content)})
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.text == f'Download <a href="/download?filename={filename}">here</a>'
|
||||||
|
|
||||||
|
r = requests.get(f"http://localhost:{self.port}/download?filename={filename}")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.text == content
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check s3: {e}')
|
||||||
|
return False
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
import io
|
||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
|
||||||
|
MOCK_DATA = [
|
||||||
|
{'name': 'John','age': 30, 'city': 'New York'},
|
||||||
|
{'name': 'Mary', 'age': 25, 'city': 'San Francisco'},
|
||||||
|
{'name': 'Peter', 'age': 45, 'city': 'Chicago'},
|
||||||
|
]
|
||||||
|
|
||||||
|
MOCK_RESULT = {
|
||||||
|
"Sheet1":{
|
||||||
|
"!ref":"A1:C4",
|
||||||
|
"A1":{"t":"s","v":"name","h":"name","w":"name"},"B1":{"t":"s","v":"age","h":"age","w":"age"},"C1":{"t":"s","v":"city","h":"city","w":"city"},
|
||||||
|
"A2":{"t":"s","v":"John","h":"John","w":"John"},"B2":{"t":"n","v":30,"w":"30"},"C2":{"t":"s","v":"New York","h":"New York","w":"New York"},
|
||||||
|
"A3":{"t":"s","v":"Mary","h":"Mary","w":"Mary"},"B3":{"t":"n","v":25,"w":"25"},"C3":{"t":"s","v":"San Francisco","h":"San Francisco","w":"San Francisco"},
|
||||||
|
"A4":{"t":"s","v":"Peter","h":"Peter","w":"Peter"},"B4":{"t":"n","v":45,"w":"45"},"C4":{"t":"s","v":"Chicago","h":"Chicago","w":"Chicago"},
|
||||||
|
"!margins":{"left":0.75,"right":0.75,"top":1,"bottom":1,"header":0.5,"footer":0.5}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class XL(Challenge):
|
||||||
|
flag_location = 'flags/xl.txt'
|
||||||
|
history_location = 'history/xl.txt'
|
||||||
|
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
try:
|
||||||
|
with open(self.flag_location, 'w') as f:
|
||||||
|
f.write(flag)
|
||||||
|
|
||||||
|
with open(self.history_location, 'a') as f:
|
||||||
|
f.write(flag + '\n')
|
||||||
|
|
||||||
|
self.logger.info(f'Flag {flag} written to {self.flag_location}')
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not write flag to {self.flag_location}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f'http://localhost:{self.port}'
|
||||||
|
files = {'file': self.generate_mock_file()}
|
||||||
|
r = requests.post(url, files=files, timeout=5)
|
||||||
|
assert r.json() == MOCK_RESULT, 'Unexpected response'
|
||||||
|
self.logger.info('Check passed for xl')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f'Could not check xl: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_mock_file(self):
|
||||||
|
memory_file = io.BytesIO()
|
||||||
|
|
||||||
|
df = pd.DataFrame(MOCK_DATA)
|
||||||
|
df.to_excel(memory_file, index=False)
|
||||||
|
|
||||||
|
memory_file.seek(0)
|
||||||
|
return memory_file
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from pydantic import BaseSettings
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
COMPOSE_LOCATION: str
|
||||||
|
ADMIN_USERNAME: str
|
||||||
|
ADMIN_PASSWORD: str
|
||||||
|
PASSWORD_10000: str
|
||||||
|
PASSWORD_11000: str
|
||||||
|
PASSWORD_12000: str
|
||||||
|
PASSWORD_13000: str
|
||||||
|
PASSWORD_14000: str
|
||||||
|
PASSWORD_15000: str
|
||||||
|
PASSWORD_16000: str
|
||||||
|
PASSWORD_17000: str
|
||||||
|
PASSWORD_18000: str
|
||||||
|
PASSWORD_19000: str
|
||||||
|
PASSWORD_20000: str
|
||||||
|
PASSWORD_21000: str
|
||||||
|
PASSWORD_22000: str
|
||||||
|
PASSWORD_23000: str
|
||||||
|
PASSWORD_24000: str
|
||||||
|
PASSWORD_25000: str
|
||||||
|
PASSWORD_26000: str
|
||||||
|
PASSWORD_27000: str
|
||||||
|
PASSWORD_28000: str
|
||||||
|
PASSWORD_29000: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_settings():
|
||||||
|
return Settings()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
class Challenge(object):
|
||||||
|
name = __name__
|
||||||
|
settings = get_settings()
|
||||||
|
port = 0
|
||||||
|
|
||||||
|
def __init__(self, port):
|
||||||
|
self.port = port
|
||||||
|
self.add_logger()
|
||||||
|
|
||||||
|
def add_logger(self):
|
||||||
|
self.logger = logging.getLogger()
|
||||||
|
|
||||||
|
def random_string(self, length):
|
||||||
|
charset = string.ascii_uppercase + string.ascii_lowercase + string.digits
|
||||||
|
return ''.join(random.choice(charset) for i in range(length))
|
||||||
|
|
||||||
|
def distribute(self, flag):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def credentials(self):
|
||||||
|
pwd = os.environ.get(f'PASSWORD_{self.port}')
|
||||||
|
if not pwd:
|
||||||
|
pwd = getattr(self.settings, f'PASSWORD_{self.port}', '')
|
||||||
|
return {
|
||||||
|
'username': 'ctfuser',
|
||||||
|
'password': pwd,
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"""SLA checkers for GEMASTIK XVII challenges (imported from
|
||||||
|
github.com/vidner/gemastik-xvii-final — no upstream receiver was provided).
|
||||||
|
|
||||||
|
Each checker validates liveness + flag presence in the container. Protocols:
|
||||||
|
- TCP/netcat : asmr, bit-canvas, go-green (xinetd banner) & ticketer (socat)
|
||||||
|
- HTTP GET : anti-alchemy, fjb, gift-card, gift-voucher, gleam-drive,
|
||||||
|
kode-viewer, more-less, tempest-poc
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .Challenge import Challenge
|
||||||
|
|
||||||
|
|
||||||
|
def _docker_exec(container: str, *args, timeout: int = 10):
|
||||||
|
try:
|
||||||
|
return subprocess.run(["docker", "exec", container, *args],
|
||||||
|
capture_output=True, text=True, timeout=timeout)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _flag_in_container(container: str, path: str = "/flag.txt") -> bool:
|
||||||
|
r = _docker_exec(container, "sh", "-c", f"test -f {path} && cat {path} || echo MISSING")
|
||||||
|
return bool(r and "MISSING" not in (r.stdout or "") and r.returncode == 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _tcp_banner(port: int, timeout: float = 4.0, expect: bytes = None) -> bool:
|
||||||
|
try:
|
||||||
|
s = socket.create_connection(("127.0.0.1", port), timeout=timeout)
|
||||||
|
s.settimeout(timeout)
|
||||||
|
data = s.recv(256)
|
||||||
|
s.close()
|
||||||
|
if expect:
|
||||||
|
return expect.lower() in data.lower()
|
||||||
|
return len(data) > 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class AntiAlchemy(Challenge):
|
||||||
|
flag_location = "flags/anti-alchemy.txt"
|
||||||
|
history_location = "history/anti-alchemy.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code in (200, 302, 500) or len(r.text) > 0
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_ANTI_ALCHEMY", "anti-alchemy_container"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Asmr(Challenge):
|
||||||
|
flag_location = "flags/asmr.txt"
|
||||||
|
history_location = "history/asmr.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
return _tcp_banner(self.port, expect=None)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BitCanvas(Challenge):
|
||||||
|
flag_location = "flags/bit-canvas.txt"
|
||||||
|
history_location = "history/bit-canvas.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
return _tcp_banner(self.port, expect=None)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Fjb(Challenge):
|
||||||
|
flag_location = "flags/fjb.txt"
|
||||||
|
history_location = "history/fjb.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_FJB", "fjb_container"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GiftCard(Challenge):
|
||||||
|
flag_location = "flags/gift-card.txt"
|
||||||
|
history_location = "history/gift-card.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_GIFT_CARD", "gift-card_container"),
|
||||||
|
"/ctf/gift-card/flag.txt")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GiftVoucher(Challenge):
|
||||||
|
flag_location = "flags/gift-voucher.txt"
|
||||||
|
history_location = "history/gift-voucher.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_GIFT_VOUCHER", "gift-voucher_container"),
|
||||||
|
"/ctf/gift-voucher/flag.txt")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GleamDrive(Challenge):
|
||||||
|
flag_location = "flags/gleam-drive.txt"
|
||||||
|
history_location = "history/gleam-drive.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_GLEAM_DRIVE", "gleam-drive_container"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class GoGreen(Challenge):
|
||||||
|
flag_location = "flags/go-green.txt"
|
||||||
|
history_location = "history/go-green.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
return _tcp_banner(self.port, expect=None)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class KodeViewer(Challenge):
|
||||||
|
flag_location = "flags/kode-viewer.txt"
|
||||||
|
history_location = "history/kode-viewer.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_KODE_VIEWER", "kode-viewer_container"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class MoreLess(Challenge):
|
||||||
|
flag_location = "flags/more-less.txt"
|
||||||
|
history_location = "history/more-less.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_MORE_LESS", "more-less_container"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TempestPoc(Challenge):
|
||||||
|
flag_location = "flags/tempest-poc.txt"
|
||||||
|
history_location = "history/tempest-poc.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
url = f"http://localhost:{self.port}/"
|
||||||
|
r = requests.get(url, timeout=6)
|
||||||
|
assert r.status_code < 500
|
||||||
|
return _flag_in_container(os.environ.get("CHALLENGE_CONTAINER_TEMPEST_POC", "tempest-poc_container"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Ticketer(Challenge):
|
||||||
|
flag_location = "flags/ticketer.txt"
|
||||||
|
history_location = "history/ticketer.txt"
|
||||||
|
|
||||||
|
def check(self):
|
||||||
|
try:
|
||||||
|
return _tcp_banner(self.port, expect=None)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
FROM python:3.11-slim-bookworm
|
||||||
|
|
||||||
|
ARG PASSWORD
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
|
||||||
|
RUN echo root:${PASSWORD} | chpasswd
|
||||||
|
COPY apt-insecure.conf /etc/apt/apt.conf.d/99gemastik-insecure
|
||||||
|
RUN apt-get -o Acquire::AllowInsecureRepositories=true update && apt-get install -y openssh-server curl nano
|
||||||
|
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||||
|
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||||
|
RUN service ssh start
|
||||||
|
RUN useradd --user-group --system --create-home --no-log-init --shell /bin/bash ctf
|
||||||
|
|
||||||
|
WORKDIR /home/ctf/app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r ./requirements.txt && rm ./requirements.txt
|
||||||
|
|
||||||
|
COPY src/ .
|
||||||
|
|
||||||
|
COPY entrypoint.sh .
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT [ "./entrypoint.sh" ]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Dumped from database version 16.3
|
||||||
|
-- Dumped by pg_dump version 16.3
|
||||||
|
|
||||||
|
SET statement_timeout = 0;
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
SET default_tablespace = '';
|
||||||
|
|
||||||
|
SET default_table_access_method = heap;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cwe; Type: TABLE; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.cwe (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE public.users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(255),
|
||||||
|
password VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE public.salt (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(255),
|
||||||
|
salt VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: cwe; Type: TABLE DATA; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO public.cwe (id,title,description) VALUES
|
||||||
|
(20,'CWE-20: Improper Input Validation','The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly.'),
|
||||||
|
(22,'CWE-22: Improper Limitation of a Pathname to a Restricted Directory (''Path Traversal'')','The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.'),
|
||||||
|
(73,'CWE-73: External Control of File Name or Path','The product allows user input to control or influence paths or file names that are used in filesystem operations.'),
|
||||||
|
(77,'CWE-77: Improper Neutralization of Special Elements used in a Command (''Command Injection'')','The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.'),
|
||||||
|
(78,'CWE-78: Improper Neutralization of Special Elements used in an OS Command (''OS Command Injection'')','The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.'),
|
||||||
|
(79,'CWE-79: Improper Neutralization of Input During Web Page Generation (''Cross-site Scripting'')','The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.'),
|
||||||
|
(88,'CWE-88: Improper Neutralization of Argument Delimiters in a Command (''Argument Injection'')','The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string.'),
|
||||||
|
(89,'CWE-89: Improper Neutralization of Special Elements used in an SQL Command (''SQL Injection'')','The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those Line truncated
|
||||||
|
(94,'CWE-94: Improper Control of Generation of Code (''Code Injection'')','The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.'),
|
||||||
|
(119,'CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer','The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer''s intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data.');
|
||||||
|
INSERT INTO public.cwe (id,title,description) VALUES
|
||||||
|
(125,'CWE-125: Out-of-bounds Read','The product reads data past the end, or before the beginning, of the intended buffer.'),
|
||||||
|
(190,'CWE-190: Integer Overflow or Wraparound','The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.'),
|
||||||
|
(200,'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor','The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.'),
|
||||||
|
(250,'CWE-250: Execution with Unnecessary Privileges','The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.'),
|
||||||
|
(266,'CWE-266: Incorrect Privilege Assignment','A product incorrectly assigns a privilege to a particular actor, creating an unintended sphere of control for that actor.'),
|
||||||
|
(269,'CWE-269: Improper Privilege Management','The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.'),
|
||||||
|
(276,'CWE-276: Incorrect Default Permissions','During installation, installed file permissions are set to allow anyone to modify those files.'),
|
||||||
|
(284,'CWE-284: Improper Access Control','The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.'),
|
||||||
|
(285,'CWE-285: Improper Authorization','The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.'),
|
||||||
|
(287,'CWE-287: Improper Authentication','When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.');
|
||||||
|
INSERT INTO public.cwe (id,title,description) VALUES
|
||||||
|
(295,'CWE-295: Improper Certificate Validation','The product does not validate, or incorrectly validates, a certificate.'),
|
||||||
|
(306,'CWE-306: Missing Authentication for Critical Function','The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.'),
|
||||||
|
(307,'CWE-307: Improper Restriction of Excessive Authentication Attempts','The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.'),
|
||||||
|
(311,'CWE-311: Missing Encryption of Sensitive Data','The product does not encrypt sensitive or critical information before storage or transmission.'),
|
||||||
|
(327,'CWE-327: Use of a Broken or Risky Cryptographic Algorithm','The product uses a broken or risky cryptographic algorithm or protocol.'),
|
||||||
|
(330,'CWE-330: Use of Insufficiently Random Values','The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.'),
|
||||||
|
(352,'CWE-352: Cross-Site Request Forgery (CSRF)','The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.'),
|
||||||
|
(362,'CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (''Race Condition'')','The product contains a code sequence that can run concurrently with other code, and the code sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence that is operating concurrently.'),
|
||||||
|
(400,'CWE-400: Uncontrolled Resource Consumption','The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.'),
|
||||||
|
(416,'CWE-416: Use After Free','The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.');
|
||||||
|
INSERT INTO public.cwe (id,title,description) VALUES
|
||||||
|
(426,'CWE-426: Untrusted Search Path','The product searches for critical resources using an externally-supplied search path that can point to resources that are not under the product''s direct control.'),
|
||||||
|
(434,'CWE-434: Unrestricted Upload of File with Dangerous Type','The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.'),
|
||||||
|
(476,'CWE-476: NULL Pointer Dereference','The product dereferences a pointer that it expects to be valid but is NULL.'),
|
||||||
|
(502,'CWE-502: Deserialization of Untrusted Data','The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.'),
|
||||||
|
(522,'CWE-522: Insufficiently Protected Credentials','The product transmits or stores authentication credentials, but it uses an insecure method that is susceptible to unauthorized interception and/or retrieval.'),
|
||||||
|
(611,'CWE-611: Improper Restriction of XML External Entity Reference','The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.'),
|
||||||
|
(732,'CWE-732: Incorrect Permission Assignment for Critical Resource','The product specifies permissions for a security-critical resource in a way that allows that resource to be read or modified by unintended actors.'),
|
||||||
|
(754,'CWE-754: Improper Check for Unusual or Exceptional Conditions','The product does not check or incorrectly checks for unusual or exceptional conditions that are not expected to occur frequently during day to day operation of the product.'),
|
||||||
|
(772,'CWE-772: Missing Release of Resource after Effective Lifetime','The product does not release a resource after its effective lifetime has ended, i.e., after the resource is no longer needed.'),
|
||||||
|
(787,'CWE-787: Out-of-bounds Write','The product writes data past the end, or before the beginning, of the intended buffer.');
|
||||||
|
INSERT INTO public.cwe (id,title,description) VALUES
|
||||||
|
(798,'CWE-798: Use of Hard-coded Credentials','The product contains hard-coded credentials, such as a password or cryptographic key.'),
|
||||||
|
(862,'CWE-862: Missing Authorization','The product does not perform an authorization check when an actor attempts to access a resource or perform an action.'),
|
||||||
|
(863,'CWE-863: Incorrect Authorization','The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.'),
|
||||||
|
(918,'CWE-918: Server-Side Request Forgery (SSRF)','The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.');
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: cwe cwe_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- PostgreSQL database dump complete
|
||||||
|
--
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
anti-alchemy:
|
||||||
|
container_name: anti-alchemy_container
|
||||||
|
hostname: anti-alchemy
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- PASSWORD=$PASSWORD_11000
|
||||||
|
volumes:
|
||||||
|
- ../receiver/flags/anti-alchemy.txt:/flag.txt:ro
|
||||||
|
- ../utils/bashrc:/root/.bashrc:ro
|
||||||
|
- ../utils/preexec.sh:/root/.preexec.sh:ro
|
||||||
|
ports:
|
||||||
|
- "11000:5000"
|
||||||
|
- "11022:22"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
environment:
|
||||||
|
- DB_NAME=postgres
|
||||||
|
- DB_USER=postgres
|
||||||
|
- DB_PASS=password
|
||||||
|
- DB_HOST=anti-alchemy-db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- SECRET_KEY=$PASSWORD_11000
|
||||||
|
depends_on:
|
||||||
|
- anti-alchemy-db
|
||||||
|
anti-alchemy-db:
|
||||||
|
image: postgres:16.3-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
volumes:
|
||||||
|
- ./db/dump.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
/usr/sbin/sshd -D &
|
||||||
|
sleep 3
|
||||||
|
python3 ./misc/init_users.py
|
||||||
|
su ctf -c "gunicorn --bind 0.0.0.0:5000 --timeout 60 --workers 6 app:app"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
PLACEHOLDER
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Flask
|
||||||
|
gunicorn
|
||||||
|
psycopg2-binary
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
class ClauseBuilder:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._queries = []
|
||||||
|
self._chars_to_sanitize = ["'", '"']
|
||||||
|
self._is_where_called = False
|
||||||
|
|
||||||
|
def _sanitize(self, q) -> str:
|
||||||
|
for c in self._chars_to_sanitize:
|
||||||
|
if c in q:
|
||||||
|
q = q.replace(c, c * 2)
|
||||||
|
return str(q).strip()
|
||||||
|
|
||||||
|
def _where(self, column, value, op) -> map:
|
||||||
|
if not self._is_where_called:
|
||||||
|
op = "WHERE"
|
||||||
|
self._is_where_called = True
|
||||||
|
return map(self._sanitize, [column, value, op])
|
||||||
|
|
||||||
|
def final(self) -> str:
|
||||||
|
q = " ".join(self._queries)
|
||||||
|
self.__init__()
|
||||||
|
return str(q).strip()
|
||||||
|
|
||||||
|
def order_by(self, column, value) -> None:
|
||||||
|
column, value = map(self._sanitize, [column, value])
|
||||||
|
self._queries.append("ORDER BY")
|
||||||
|
self._queries.append('"' + column + '"')
|
||||||
|
self._queries.append(value)
|
||||||
|
|
||||||
|
def select(self, table) -> None:
|
||||||
|
table = self._sanitize(table)
|
||||||
|
self._queries.append("SELECT")
|
||||||
|
self._queries.append("*")
|
||||||
|
self._queries.append("FROM")
|
||||||
|
self._queries.append('"' + table + '"')
|
||||||
|
|
||||||
|
def where(self, column, value, cmp="ILIKE", op="AND") -> None:
|
||||||
|
column, value, op = self._where(column, value, op)
|
||||||
|
self._queries.append(op)
|
||||||
|
self._queries.append('"' + column + '"')
|
||||||
|
if column == "id":
|
||||||
|
self._queries.append("=")
|
||||||
|
self._queries.append(str(int(value)))
|
||||||
|
elif cmp == "EQ":
|
||||||
|
self._queries.append("=")
|
||||||
|
self._queries.append("'" + value + "'")
|
||||||
|
else:
|
||||||
|
self._queries.append("ILIKE")
|
||||||
|
self._queries.append("'%" + value + "%'")
|
||||||
|
|
||||||
|
|
||||||
|
class QueryBuilder:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._cb = ClauseBuilder()
|
||||||
|
self._obj = {}
|
||||||
|
self._defined_keys = ["table", "columns"]
|
||||||
|
self._sorting_values = ["asc", "desc"]
|
||||||
|
self._login_keys = ["username"]
|
||||||
|
|
||||||
|
def _order_by(self) -> None:
|
||||||
|
for value, column in self._obj.items():
|
||||||
|
if value in self._sorting_values:
|
||||||
|
self._cb.order_by(column, value)
|
||||||
|
break
|
||||||
|
|
||||||
|
def _select(self) -> None:
|
||||||
|
self._cb.select(self._obj["table"])
|
||||||
|
|
||||||
|
def _where(self) -> None:
|
||||||
|
for column, value in self._obj.items():
|
||||||
|
if column in self._defined_keys + self._sorting_values:
|
||||||
|
continue
|
||||||
|
elif column in self._login_keys:
|
||||||
|
self._cb.where(column, value, "EQ")
|
||||||
|
else:
|
||||||
|
self._cb.where(column, value)
|
||||||
|
|
||||||
|
def generate(self, obj) -> str:
|
||||||
|
self._obj = obj
|
||||||
|
self._select()
|
||||||
|
self._where()
|
||||||
|
self._order_by()
|
||||||
|
return self._cb.final()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
from flask import Flask, render_template, session, redirect, url_for
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
from antialchemy import *
|
||||||
|
from helper import *
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config["SECRET_KEY"] = environ.get("SECRET_KEY", "SECRET_KEY")
|
||||||
|
app.config["PERMANENT_SESSION_LIFETIME"] = 3 * 60
|
||||||
|
qb = QueryBuilder()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/flag")
|
||||||
|
@check_login_status
|
||||||
|
def flag():
|
||||||
|
if session["user"] == "admin":
|
||||||
|
try:
|
||||||
|
return make_resp(200, open("/flag.txt").read())
|
||||||
|
except:
|
||||||
|
return make_resp(500, "Flag not found, please contact problem setter")
|
||||||
|
|
||||||
|
return make_resp(401, "You need to log in as admin to get the flag")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/login")
|
||||||
|
@check_request_body
|
||||||
|
def login(*args, **kwargs):
|
||||||
|
payload = kwargs.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = create_db_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
qb.generate(
|
||||||
|
{
|
||||||
|
"table": "users",
|
||||||
|
"username": payload["username"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
return make_resp(401, "Invalid username/password")
|
||||||
|
|
||||||
|
_, username, password_hash = row
|
||||||
|
password = payload.pop("password")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
qb.generate(
|
||||||
|
{
|
||||||
|
"table": "salt",
|
||||||
|
"username": username,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
return make_resp(401, "Invalid username/password")
|
||||||
|
|
||||||
|
_, _, salt = row
|
||||||
|
|
||||||
|
if not check_password_hash(password, salt, password_hash):
|
||||||
|
return make_resp(401, "Invalid username/password")
|
||||||
|
|
||||||
|
session.clear()
|
||||||
|
session["user"] = username
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception: {e}")
|
||||||
|
return make_resp(400, "Bad Request")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/logout")
|
||||||
|
@check_login_status
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/view")
|
||||||
|
@check_login_status
|
||||||
|
@check_request_body
|
||||||
|
def view(*args, **kwargs):
|
||||||
|
payload = kwargs.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = create_db_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(qb.generate(payload | {"table": "cwe"}))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return make_resp(200, "OK", {"rows": rows})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception: {e}")
|
||||||
|
return make_resp(400, "Bad Request")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def index():
|
||||||
|
try:
|
||||||
|
if session["user"]:
|
||||||
|
return render_template("dashboard.html")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return render_template("login.html")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from flask import request, session
|
||||||
|
from functools import wraps
|
||||||
|
from hashlib import sha1
|
||||||
|
from json import dumps
|
||||||
|
from os import environ
|
||||||
|
from psycopg2 import connect
|
||||||
|
from string import printable
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
def create_db_conn():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return connect(
|
||||||
|
database=environ.get("DB_NAME", "postgres"),
|
||||||
|
user=environ.get("DB_USER", "postgres"),
|
||||||
|
password=environ.get("DB_PASS", "password"),
|
||||||
|
host=environ.get("DB_HOST", "localhost"),
|
||||||
|
port=environ.get("DB_PORT", "5432"),
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def make_resp(status, message, data=None):
|
||||||
|
return {"status": status, "message": message, "data": data}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_password_hash(password, salt):
|
||||||
|
return sha1((salt + password).encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def check_password_hash(password, salt, password_hash):
|
||||||
|
return generate_password_hash(password, salt) == password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def check_login_status(f):
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
session["user"]
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception: {e}")
|
||||||
|
return make_resp(401, "Unauthorized")
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def check_request_body(f):
|
||||||
|
check_blist = lambda s: all(x not in s.lower() for x in ["pg_"])
|
||||||
|
check_wlist = lambda s: all(c in printable for c in s)
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
data = {k: v for k, v in data.items() if data.get(k)}
|
||||||
|
|
||||||
|
dd = dumps(data, separators=(",", ":"))
|
||||||
|
assert len(dd) < 256 and check_blist(dd), "Bad payload"
|
||||||
|
|
||||||
|
for item in data.items():
|
||||||
|
assert all(map(check_wlist, item)), "Bad payload"
|
||||||
|
|
||||||
|
kwargs.update(data)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception: {e}")
|
||||||
|
return make_resp(400, "Bad Request")
|
||||||
|
|
||||||
|
return inner
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import sys
|
||||||
|
sys.path += [".", ".."]
|
||||||
|
|
||||||
|
from helper import create_db_conn
|
||||||
|
from os import environ
|
||||||
|
|
||||||
|
print("Getting environment variables...")
|
||||||
|
print(environ.get("SECRET_KEY", "SECRET_KEY"))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Getting rows of users and salt...")
|
||||||
|
conn = create_db_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("SELECT * FROM users")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
cur.execute("SELECT * FROM salt")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Done")
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import sys
|
||||||
|
sys.path += [".", ".."]
|
||||||
|
|
||||||
|
from helper import create_db_conn, generate_password_hash
|
||||||
|
from os import environ, urandom
|
||||||
|
|
||||||
|
conn = create_db_conn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
users = [
|
||||||
|
("admin", environ.get("SECRET_KEY", "SECRET_KEY"), urandom(8).hex()),
|
||||||
|
("gemastik", "P@ssw0rd", urandom(8).hex()),
|
||||||
|
]
|
||||||
|
|
||||||
|
for username, password, salt in users:
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO users (username, password) VALUES (%s, %s)",
|
||||||
|
(username, generate_password_hash(password, salt)),
|
||||||
|
)
|
||||||
|
cur.execute("INSERT INTO salt (username, salt) VALUES (%s, %s)", (username, salt))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.sortable-column,button,strong{cursor:pointer}a,strong{color:#44509e}#pagination,#search{border:var(--debug);gap:4px;width:300px;padding:4px}*{box-sizing:border-box}body{font-family:Arial,Helvetica,sans-serif}table{width:100%;border-collapse:collapse}td,th{padding:.5rem;text-align:left;border-bottom:1px solid #ddd}td{height:6rem}a{text-decoration:none;font-weight:700}button:disabled{cursor:not-allowed}.container{height:inherit;display:flex;flex-direction:column;justify-content:space-between;borLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
const step=10;var rawResp={},currentData=[],totalData=[],currentPage=0,totalPage=Math.floor((rawResp.length+9)/10),pload={};function fillBody(e,t){let a=document.getElementById("fillable-body");a.innerHTML="",e.forEach((e,n)=>{let r=document.createElement("tr"),l=document.createElement("td"),i=document.createElement("td"),c=document.createElement("td"),o=document.createElement("td"),s=document.createElement("a");l.textContent=n+t,s.href=`https://cwe.mitre.org/data/definitions/${e[0]}.html`,s.teLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.form-group button{width:100%}.container{max-width:640px;position:absolute;top:50%;left:50%;-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}h1{text-align:center}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
document.getElementById("form-submit").addEventListener("click",async function(e){e.preventDefault();let t=document.getElementById("form-username").value,a=document.getElementById("form-password").value;if(!t||!a){alert("Username/password cannot be empty");return}try{let n=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t,password:a})}),o=await n.json();alert(o.message)}catch(r){console.log(r),window.location.reload()}});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>List of Top CWE - Common Weakness Enumeration</title>
|
||||||
|
<link rel="icon" href="data:," />
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard.min.css') }}" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="atas">
|
||||||
|
<form action="javascript:search()" id="search">
|
||||||
|
<label for="code">CWE ID<input type="number" name="CWE ID" id="cwe-id" min="0" autofocus /></label>
|
||||||
|
<label for="title">Title<input type="text" name="Title" id="title" maxlength="255" /></label>
|
||||||
|
<label for="description">Description<input type="text" name="Description" id="description"
|
||||||
|
maxlength="255" /></label>
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
<div style="margin: auto; text-align: center;">
|
||||||
|
<p>
|
||||||
|
Welcome to <span style="font-weight: bold">List of Top CWE</span>,
|
||||||
|
{{ session["user"] }}!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Click here to access admin <strong id="access-flag">flag</strong> or
|
||||||
|
<strong id="logout">logout</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form id="pagination">
|
||||||
|
<button type="submit" id="prev-page">Prev</button>
|
||||||
|
<p id="page"></p>
|
||||||
|
<button type="submit" id="next-page">Next</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="bawah">
|
||||||
|
<table class="sortable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th class="sortable-column" onclick="javascript:sort(0)">
|
||||||
|
CWE ID
|
||||||
|
</th>
|
||||||
|
<th class="sortable-column" onclick="javascript:sort(1)">
|
||||||
|
Title
|
||||||
|
</th>
|
||||||
|
<th class="sortable-column" onclick="javascript:sort(2)">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="fillable-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script src="{{ url_for('static', filename='dashboard.min.js') }}"></script>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Login - Common Weakness Enumeration</title>
|
||||||
|
<link rel="icon" href="data:," />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" />
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='login.min.css') }}">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form>
|
||||||
|
<div class="form-group pt-3">
|
||||||
|
<input type="text" class="form-control" id="form-username" placeholder="Username" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group pt-3">
|
||||||
|
<input type="password" class="form-control" id="form-password" placeholder="Password" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group pt-3">
|
||||||
|
<button type="submit" class="btn btn-primary" id="form-submit">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script src="{{ url_for('static', filename='login.min.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
FROM ruby:3.2-slim-bookworm
|
||||||
|
|
||||||
|
ARG PASSWORD
|
||||||
|
|
||||||
|
RUN echo root:${PASSWORD} | chpasswd
|
||||||
|
COPY apt-insecure.conf /etc/apt/apt.conf.d/99gemastik-insecure
|
||||||
|
RUN apt-get -o Acquire::AllowInsecureRepositories=true update && apt-get install -y openssh-server curl
|
||||||
|
RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config
|
||||||
|
RUN echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||||
|
RUN service ssh start
|
||||||
|
|
||||||
|
WORKDIR /ctf/art/
|
||||||
|
|
||||||
|
RUN useradd -m ctf
|
||||||
|
RUN chown -R root:root /ctf/art/
|
||||||
|
|
||||||
|
COPY Gemfile .
|
||||||
|
COPY app.rb .
|
||||||
|
COPY start.sh .
|
||||||
|
|
||||||
|
RUN bundle install
|
||||||
|
RUN touch /flag.txt
|
||||||
|
|
||||||
|
RUN chmod +x start.sh
|
||||||
|
CMD ./start.sh
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
source "http://rubygems.org"
|
||||||
|
|
||||||
|
gem "sinatra"
|
||||||
|
gem "slim"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
require "sinatra"
|
||||||
|
require "slim"
|
||||||
|
|
||||||
|
set :port, 8080
|
||||||
|
set :bind, '0.0.0.0'
|
||||||
|
set :environment, :production
|
||||||
|
|
||||||
|
get '/' do
|
||||||
|
redirect '/art/gemastik'
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/art/:word' do
|
||||||
|
return Slim::Template.new{ '<iframe height="100%" width="100%" frameborder="0" src=https://asciified.thelicato.io/api/v2/ascii?text=' + params[:word] + '></iframe>' }.render
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Acquire::AllowInsecureRepositories "true";
|
||||||
|
Acquire::AllowDowngradeToInsecureRepositories "true";
|
||||||
|
Apt::Get::AllowUnauthenticated "true";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
art:
|
||||||
|
container_name: art_container
|
||||||
|
hostname: art
|
||||||
|
restart: always
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- PASSWORD=$PASSWORD_10000
|
||||||
|
volumes:
|
||||||
|
- ../receiver/flags/art.txt:/flag.txt:ro
|
||||||
|
- ../utils/bashrc:/root/.bashrc:ro
|
||||||
|
- ../utils/preexec.sh:/root/.preexec.sh:ro
|
||||||
|
ports:
|
||||||
|
- "10000:8080"
|
||||||
|
- "10022:22"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# run sshd
|
||||||
|
/usr/sbin/sshd -D &
|
||||||
|
# run the command
|
||||||
|
su ctf -c "bundle install"
|
||||||
|
su ctf -c "ruby /ctf/art/app.rb"
|
||||||
Loaded 50 of 1317 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user