Replaces the 3-line upstream stub with a manual that documents the platform as it actually runs. Every claim is derived from the live code and registry rather than from memory. Challenge spec: - 28-challenge tables (6 XVIII / 10 XVI / 12 XVII, 16 active) generated from teams/challenge_registry.json, with per-challenge org_port, chall/ssh offsets, and the real team-1 runtime ports read from state.json. - Port formula corrected to the real one: port = 30000 + idx*1000 + chall_offset. org_port is the native graveyard port and is NOT used for runtime allocation, so two challenges sharing an org_port (carbeat offset 1 vs anti-alchemy offset 30) never collide. - Per-challenge ssh_user documented: only the 6 native XVIII images provision ctfuser; all imported XVI/XVII images chpasswd root, so hardcoding ctfuser breaks 10 of the 16 active challenges. - Scoring: 100 per flag awarded to the ATTACKER (first solve only), +50 SLA bonus at most once per 5-minute window, runtime threshold documented as len(enabled_challenges()) rather than the hardcoded constant 6. Setup and operations: - Setup from clone: required /opt path, Docker, venv, panel credentials, both systemd units verbatim, team creation, verification step. - Full HTTP API split into public / admin / team, including why challenge toggle and bulk team delete are async jobs. - Troubleshooting and operational traps as declarative rules: the bare domain is the receiver and not the panel, EOL base images, UFW default-deny silently blackholing ports, the mandatory compose -p teamN project name, and why docker image prune -af destroys services-* images that are in use. - Image sizes measured from the host (189MB-903MB, ~8GB for 16 active) instead of the incorrect "~3GB per challenge" figure. - Topology section: PixiJS v8, on-demand rendering, and the parent-to-child drag hierarchy derived from the edge list. The flag example is redacted to a placeholder. No live credential, token, or flag is committed. README.md is the only file touched.
641 lines
23 KiB
Markdown
641 lines
23 KiB
Markdown
# Attack Defense Platform
|
||
|
||
Platform attack-defense (serang–serang) untuk GEMASTIK Final Round, menyatukan
|
||
**28 challenge** dari tiga set (**GEMASTIK XVIII**, **XVI**, **XVII**) di bawah
|
||
satu panel admin, satu flag store, satu SLA checker, dan satu skorboard.
|
||
|
||
Multi-team: N tim, masing-masing memiliki copy semua challenge + flag sendiri,
|
||
dengan receiver terisolasi per tim, checker SLA otomatis, dan visualisasi
|
||
topologi serangan real-time.
|
||
|
||
```
|
||
Browser (admin/team)
|
||
| HTTP + WebSocket (proxy server-side)
|
||
v
|
||
Panel :18081 (FastAPI) ---- systemd: gemastik-panel
|
||
|
|
||
+--> Receiver global :18080 ---- systemd: gemastik-receiver
|
||
+--> Receiver tim N :31080+1000*(N-1) ---- systemd: gemastik-receiver-teamN
|
||
| (menjalankan checker SLA untuk tim N)
|
||
+--> N x <challenge>_container_teamN ---- docker compose
|
||
|
|
||
+--> flags + leaderboard + points (teams/leaderboard.json, teams/points.json)
|
||
```
|
||
|
||
---
|
||
|
||
## Daftar Isi
|
||
|
||
- [Arsitektur](#arsitektur)
|
||
- [Spesifikasi Challenge](#spesifikasi-challenge)
|
||
- [Spesifikasi Port](#spesifikasi-port)
|
||
- [Skor dan Penilaian](#skor-dan-penilaian)
|
||
- [Kebutuhan Sistem](#kebutuhan-sistem)
|
||
- [Setup](#setup)
|
||
- [Operasional Harian](#operasional-harian)
|
||
- [Topologi](#topologi)
|
||
- [HTTP API](#http-api)
|
||
- [Troubleshooting](#troubleshooting)
|
||
- [Jebakan Operasional](#jebakan-operasional)
|
||
- [Struktur Direktori](#struktur-direktori)
|
||
|
||
---
|
||
|
||
## Arsitektur
|
||
|
||
Tiga proses inti, semuanya dikelola systemd:
|
||
|
||
| Proses | Port | Unit systemd | Peran |
|
||
|---|---|---|---|
|
||
| Panel admin | **18081** | `gemastik-panel` | Web UI admin + seluruh API |
|
||
| Receiver global | **18080** | `gemastik-receiver` | Flag store untuk mode single-node |
|
||
| Receiver tim N | **31080 + 1000×(N−1)** | `gemastik-receiver-teamN` | Checker SLA tim N |
|
||
|
||
**Mengapa receiver per tim harus unit terpisah.** Kalau receiver dijalankan
|
||
sebagai child process dari panel, `systemctl restart gemastik-panel` akan
|
||
membunuh seluruh cgroup — termasuk semua receiver — dan SLA semua tim ikut
|
||
turun ke 0. Unit terpisah membuat restart panel tidak menyentuh receiver.
|
||
Generatornya: `panel/gen_receiver_services.py`.
|
||
|
||
**Kredensial tidak pernah masuk browser.** Panel melakukan proxy ke receiver
|
||
secara server-side, sehingga password admin hanya ada di `panel/.env` pada host.
|
||
|
||
**Sumber data tunggal.** `teams/challenge_registry.json` dibaca oleh panel,
|
||
generator compose, dan generator receiver. Menambah challenge = menambah satu
|
||
entri di registry, bukan menyunting tiga tempat.
|
||
|
||
### Alur satu flag
|
||
|
||
```
|
||
tim penyerang submit flag
|
||
-> panel POST /api/flag/submit
|
||
-> baca flag tim target dari receiver
|
||
-> cocok?
|
||
ya -> catat di leaderboard + skor untuk PENYERANG
|
||
tidak -> tolak
|
||
```
|
||
|
||
Flag di-mint per (tim, challenge), bukan satu flag global.
|
||
|
||
---
|
||
|
||
## Spesifikasi Challenge
|
||
|
||
**28 challenge terdaftar, 16 aktif secara default.**
|
||
|
||
Kolom `Port team 1` / `SSH team 1` diisi `-` untuk challenge nonaktif karena
|
||
port-nya baru dialokasikan saat challenge diaktifkan.
|
||
|
||
### GEMASTIK XVIII — 6 challenge, 6 aktif
|
||
|
||
User SSH: `ctfuser`.
|
||
|
||
| # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 1 | `blogpost` | web | 10000 | 0/22 | 31000 | 31022 | aktif |
|
||
| 2 | `carbeat` | pwn | 11000 | 1/23 | 31001 | 31023 | aktif |
|
||
| 3 | `cdn` | web | 12000 | 2/24 | 31002 | 31024 | aktif |
|
||
| 4 | `phew` | crypto | 13000 | 3/25 | 31003 | 31025 | aktif |
|
||
| 5 | `sheesh` | crypto | 14000 | 4/26 | 31004 | 31026 | aktif |
|
||
| 6 | `warmup` | warmup | 15000 | 5/27 | 31005 | 31027 | aktif |
|
||
|
||
### GEMASTIK XVI — 10 challenge, 3 aktif
|
||
|
||
User SSH: `root`.
|
||
|
||
| # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 7 | `art` | web | 10000 | 10/110 | 31010 | 31110 | aktif |
|
||
| 8 | `xl` | web | 11000 | 11/111 | 31011 | 31111 | aktif |
|
||
| 9 | `gemas-notes` | web | 12000 | 12/112 | - | - | nonaktif |
|
||
| 10 | `pasta` | web | 13000 | 13/113 | - | - | nonaktif |
|
||
| 11 | `burvesigner` | crypto | 14000 | 14/114 | - | - | nonaktif |
|
||
| 12 | `hirnfick` | pwn | 15000 | 15/115 | - | - | nonaktif |
|
||
| 13 | `gemas-fetcher` | web | 16000 | 16/116 | - | - | nonaktif |
|
||
| 14 | `s3` | web | 20000 | 20/120 | 31020 | 31120 | aktif |
|
||
| 15 | `crawlback` | web | 21000 | 21/121 | - | - | nonaktif |
|
||
| 16 | `back-to-basic` | warmup | 22000 | 22/122 | - | - | nonaktif |
|
||
|
||
### GEMASTIK XVII — 12 challenge, 7 aktif
|
||
|
||
User SSH: `root`.
|
||
|
||
| # | Challenge | Kategori | org_port | Offset chall/SSH | Port team 1 | SSH team 1 | Status |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 17 | `anti-alchemy` | web | 11000 | 30/130 | 31030 | 31130 | aktif |
|
||
| 18 | `asmr` | pwn | 15000 | 31/131 | - | - | nonaktif |
|
||
| 19 | `bit-canvas` | pwn | 20000 | 32/132 | 31032 | 31132 | aktif |
|
||
| 20 | `fjb` | web-pwn | 17000 | 33/133 | - | - | nonaktif |
|
||
| 21 | `gift-card` | crypto | 21000 | 34/134 | 31034 | 31134 | aktif |
|
||
| 22 | `gift-voucher` | crypto | 16000 | 35/135 | 31035 | 31135 | aktif |
|
||
| 23 | `gleam-drive` | web-crypto | 12000 | 36/136 | 31036 | 31136 | aktif |
|
||
| 24 | `go-green` | rev | 20000 | 37/137 | - | - | nonaktif |
|
||
| 25 | `kode-viewer` | web | 10000 | 38/138 | - | - | nonaktif |
|
||
| 26 | `more-less` | web | 22000 | 39/139 | 31039 | 31139 | aktif |
|
||
| 27 | `tempest-poc` | web | 14080 | 40/140 | - | - | nonaktif |
|
||
| 28 | `ticketer` | crypto | 14000 | 41/141 | 31041 | 31141 | aktif |
|
||
|
||
Kategori: 13 web, 6 crypto, 4 pwn, 2 warmup, dan masing-masing satu
|
||
web-pwn, web-crypto, rev. `gleam-drive` dilayani lewat HTTPS (field `scheme`
|
||
di registry), 27 challenge lainnya HTTP.
|
||
|
||
### Format flag
|
||
|
||
```
|
||
GEMASTIK18{TEAM<idx>_<CHALLENGE>_<12 hex>}
|
||
|
||
contoh: GEMASTIK18{TEAM1_BLOGPOST_<12 hex acak>}
|
||
```
|
||
|
||
### User SSH per challenge
|
||
|
||
Hanya 6 challenge native GEMASTIK XVIII yang membuat user `ctfuser`. Semua
|
||
challenge impor XVI/XVII menjalankan `echo root:${PASSWORD} | chpasswd` di
|
||
Dockerfile, sehingga login sebagai `ctfuser` ditolak walaupun password benar.
|
||
|
||
Field `ssh_user` di `teams/challenge_registry.json` yang menentukan ini, dibaca
|
||
`panel/teams.py` saat `set_ssh_passwords()`. Men-hardcode `ctfuser` membuat 10
|
||
dari 16 challenge gagal login padahal `state.json` terlihat benar.
|
||
|
||
---
|
||
|
||
## Spesifikasi Port
|
||
|
||
Setiap tim mendapat blok port sendiri, dengan basis 30000 dan langkah 1000:
|
||
|
||
```
|
||
port_challenge(tim i, challenge c) = 30000 + 1000 x i + chall_offset(c)
|
||
port_ssh(tim i, challenge c) = 30000 + 1000 x i + ssh_offset(c)
|
||
port_receiver(tim i) = 30000 + 1000 x i + 80
|
||
```
|
||
|
||
`chall_offset` dan `ssh_offset` dibaca dari `teams/challenge_registry.json`
|
||
(`panel/teams.py`, `create_team()`). Field `org_port` di registry adalah port
|
||
native challenge di graveyard asalnya dan **tidak dipakai** untuk menghitung
|
||
port runtime.
|
||
|
||
Enam challenge native GEMASTIK XVIII memakai offset challenge 0–5 dan SSH
|
||
22–27, sehingga untuk team 1 berada di 31000–31005 dan 31022–31027:
|
||
|
||
| Tim | Port challenge | Port SSH | Receiver |
|
||
|---|---|---|---|
|
||
| 1 | 31000–31005 | 31022–31027 | 31080 |
|
||
| 2 | 32000–32005 | 32022–32027 | 32080 |
|
||
| 3 | 33000–33005 | 33022–33027 | 33080 |
|
||
| 4 | 34000–34005 | 34022–34027 | 34080 |
|
||
|
||
Challenge impor memakai offset sendiri, jadi portnya tidak selalu berakhiran
|
||
`0000`–`0005`. Angka nyata team 1: `art` 31010/31110, `xl` 31011/31111,
|
||
`s3` 31020/31120, `anti-alchemy` 31030/31130, `bit-canvas` 31032/31132,
|
||
`gift-card` 31034/31134, `gift-voucher` 31035/31135,
|
||
`gleam-drive` 31036/31136, `more-less` 31039/31139, `ticketer` 31041/31141.
|
||
|
||
Dua challenge dengan `org_port` sama tidak bentrok, karena yang dipakai adalah
|
||
`chall_offset`. `anti-alchemy` (XVII, offset 30) dan `carbeat` (XVIII,
|
||
offset 1) sama-sama punya `org_port` 11000, tetapi memakai port 31030 dan
|
||
31001.
|
||
|
||
---
|
||
|
||
## Skor dan Penilaian
|
||
|
||
```python
|
||
POINTS_PER_FLAG = 100 # ke tim PENYERANG, hanya solve pertama
|
||
SLA_BONUS_POINTS = 50 # bonus bila semua challenge aktif UP
|
||
SLA_BONUS_MIN_ALIVE = 6 # konstanta; threshold runtime = len(enabled_challenges())
|
||
```
|
||
|
||
**Attack points.** Submit flag benar milik tim lain memberi +100 ke tim
|
||
penyerang. Duplikat (flag + penyerang + target sama) tidak dihitung dua kali.
|
||
|
||
**SLA bonus.** Diberi bila semua challenge yang aktif UP, maksimal sekali per
|
||
jendela 5 menit. Threshold runtime bukan angka tetap 6 melainkan
|
||
`len(enabled_challenges())` — mengaktifkan challenge ke-17 membuat syaratnya
|
||
"semua 17 UP".
|
||
|
||
**Badge.** Juara, runner-up, dan tempat ketiga dihitung dari total poin.
|
||
|
||
---
|
||
|
||
## Kebutuhan Sistem
|
||
|
||
Host reference: Ubuntu 24.04 (noble), x86_64, Docker + Compose v2, systemd.
|
||
|
||
| Sumber daya | Minimum | Recommended |
|
||
|---|---|---|
|
||
| CPU | 2 vCPU | 4 vCPU |
|
||
| RAM | 8 GB | 16 GB |
|
||
| Disk | 60 GB | 100 GB+ |
|
||
| Docker | Compose v2 (`docker compose`) | — |
|
||
|
||
Compose v1 (`docker-compose`) tidak didukung — seluruh generator memakai
|
||
`docker compose`.
|
||
|
||
Disk adalah pembatas utama. Tiap challenge yang aktif menjadi satu image
|
||
`services-<name>`; ukurannya bervariasi dari ~190 MB (`gift-card`) sampai ~900 MB
|
||
(`warmup`), dan pada host ini 16 image aktif menempati sekitar 8 GB. Membangun
|
||
banyak challenge sekaligus akan mengisi disk sebelum selesai — implementasi
|
||
terbaik adalah membangun challenge secara berurutan dan menjalankan
|
||
`docker builder prune -af` di antaranya.
|
||
|
||
`phew` menjalankan generator kunci Paillier saat start (±12 detik) sehingga
|
||
butuh RAM ekstra dan checker-nya memakai `_CRYPTO_TIMEOUT`, bukan timeout prompt
|
||
bawaan 5 detik.
|
||
|
||
Prasyarat jaringan: setiap compose template sudah memuat
|
||
`extra_hosts: host.docker.internal:host-gateway`, dan UFW host harus
|
||
mengizinkan port challenge (lihat [Jebakan Operasional](#jebakan-operasional)).
|
||
|
||
Dependency checker ada di `receiver/requirements.txt`: fastapi, uvicorn,
|
||
pwntools, pyelftools, pycryptodome, fastecdsa, ecdsa, Pillow, pandas, openpyxl,
|
||
PyPDF2.
|
||
|
||
---
|
||
|
||
## Setup
|
||
|
||
### 1. Clone
|
||
|
||
```bash
|
||
git clone <repo-url> attack-defense-platform
|
||
cd attack-defense-platform
|
||
```
|
||
|
||
Semua path di dalam kode memakai `/opt/gemastik18-final` sebagai `BASE`, jadi
|
||
letakkan repo di sana:
|
||
|
||
```bash
|
||
sudo mkdir -p /opt
|
||
sudo mv attack-defense-platform /opt/gemastik18-final
|
||
cd /opt/gemastik18-final
|
||
```
|
||
|
||
### 2. Docker
|
||
|
||
```bash
|
||
sudo bash node.sh
|
||
```
|
||
|
||
`node.sh` memasang Docker CE dari repo resmi lalu menjalankan `starter.py`.
|
||
Instalasi manual:
|
||
|
||
```bash
|
||
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
|
||
docker-buildx-plugin docker-compose-plugin
|
||
```
|
||
|
||
### 3. Kredensial panel
|
||
|
||
```bash
|
||
cat > panel/.env <<'EOF'
|
||
PANEL_ADMIN_USER=admin
|
||
PANEL_ADMIN_PASS=ganti-dengan-password-kuat
|
||
EOF
|
||
chmod 600 panel/.env
|
||
```
|
||
|
||
`panel/.env` sudah masuk `.gitignore` dan tidak pernah ter-commit.
|
||
|
||
### 4. Python environment
|
||
|
||
```bash
|
||
cd /opt/gemastik18-final/receiver
|
||
sudo python3 -m venv .venv
|
||
sudo .venv/bin/pip install -r requirements.txt
|
||
```
|
||
|
||
### 5. Unit systemd
|
||
|
||
Panel (:18081):
|
||
|
||
```ini
|
||
# /etc/systemd/system/gemastik-panel.service
|
||
[Unit]
|
||
Description=Gemastik A/D Panel (web UI for receiver)
|
||
After=gemastik-receiver.service network-online.target
|
||
Wants=gemastik-receiver.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
WorkingDirectory=/opt/gemastik18-final/panel
|
||
EnvironmentFile=-/opt/gemastik18-final/panel/.env
|
||
ExecStart=/opt/gemastik18-final/receiver/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 18081
|
||
Restart=always
|
||
RestartSec=5
|
||
Environment=PYTHONUNBUFFERED=1
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
Receiver global (:18080):
|
||
|
||
```ini
|
||
# /etc/systemd/system/gemastik-receiver.service
|
||
[Unit]
|
||
Description=Gemastik18 Receiver Service (CTF flag/control API)
|
||
After=docker.service network-online.target
|
||
Wants=docker.service
|
||
Requires=docker.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
WorkingDirectory=/opt/gemastik18-final/receiver
|
||
ExecStart=/opt/gemastik18-final/receiver/.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 18080
|
||
Restart=always
|
||
RestartSec=5
|
||
Environment=PYTHONUNBUFFERED=1
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now gemastik-receiver gemastik-panel
|
||
systemctl is-active gemastik-panel gemastik-receiver
|
||
```
|
||
|
||
### 6. Buat tim
|
||
|
||
Lewat UI (**Teams** tab, admin login) atau API:
|
||
|
||
```bash
|
||
curl -X POST http://127.0.0.1:18081/api/teams/set \
|
||
-H 'Content-Type: application/json' \
|
||
-b cookies.txt -c cookies.txt \
|
||
-d '{"count":2,"labels":{"1":"Tim Satu","2":"Tim Dua"}}'
|
||
```
|
||
|
||
Endpoint ini idempoten (membuat yang hilang, mempertahankan yang ada) dan
|
||
otomatis menjalankan `sync_team_ufw()` untuk setiap tim baru — tanpa itu port
|
||
tim akan di-blackhole UFW.
|
||
|
||
### 7. Verifikasi
|
||
|
||
```bash
|
||
bash panel/verify_platform_health.py
|
||
```
|
||
|
||
---
|
||
|
||
## Operasional Harian
|
||
|
||
| Aksi | Perintah |
|
||
|---|---|
|
||
| Lihat status semua service | `systemctl is-active gemastik-panel gemastik-receiver gemastik-receiver-team*` |
|
||
| Restart panel | `systemctl restart gemastik-panel` |
|
||
| Sinkronkan container tim dengan registry | `bash panel/apply_registry.sh` |
|
||
| Health check | `python3 panel/verify_platform_health.py` |
|
||
| SSH round-trip ke semua challenge | `python3 panel/verify_ssh_e2e.py` |
|
||
| Cek user SSH per challenge | `bash panel/audit_ssh_users.sh` |
|
||
| Reset penuh (tim, flag, kredensial) | `bash panel/reset_runtime.sh` |
|
||
| Health suite topologi | `bash panel/verify_topo_full.sh` |
|
||
| Beban host | `bash panel/watch_load.sh` |
|
||
|
||
**Reverse proxy.** Domain challenge dan panel dilayani Traefik lewat file
|
||
dynamic di `/data/coolify/proxy/dynamic/attackdefense.yaml`. Pola service:
|
||
|
||
```yaml
|
||
services:
|
||
gemastik-panel-service:
|
||
loadBalancer:
|
||
servers:
|
||
- url: "http://host.docker.internal:18081"
|
||
```
|
||
|
||
Cert TLS terbit otomatis lewat `certResolver: letsencrypt` selama DNS
|
||
terresolve dan port 80 terbuka.
|
||
|
||
Domain yang dipakai di host ini: `panel.attackdefense.imrnes.team` (panel, :18081)
|
||
dan `attackdefense.imrnes.team` (receiver global, :18080), plus subdomain
|
||
per challenge aktif. Perhatikan domain bare menunjuk ke receiver, bukan panel —
|
||
`/login` di sana akan 404 dan terlihat seperti panel mati.
|
||
|
||
---
|
||
|
||
## Topologi
|
||
|
||
Tab **Topology** merender graf serangan antar tim dengan PixiJS v8
|
||
(`panel/static/topo_pixi.js`, engine di `panel/static/vendor/pixi.mjs`).
|
||
|
||
- Pan, zoom, dan drag berjalan lewat satu funnel `applyView()`.
|
||
- Drag node tim menyeret seluruh challenge-nya. Indeks parent→child dibangun
|
||
dari edge list — sumber yang sama untuk menggambar garis — sehingga hierarki
|
||
drag tidak mungkin berbeda dari gambar.
|
||
- Ticker Pixi didaftarkan tapi tidak dinyalakan: render berlangsung on demand
|
||
(hanya saat ada pulse serangan atau sedang drag), lalu berhenti saat sunyi.
|
||
Pada host tanpa GPU, repaint 60fps atas scene statis membuat halaman tidak
|
||
merespons (rAF turun ke 2 FPS, lag `setTimeout(0)` 1353 ms).
|
||
- Posisi drag kembali ke layout otomatis saat data di-refresh tiap 10 detik.
|
||
Untuk merender ulang objek, `.text` hanya di-set bila string benar-benar
|
||
berubah — setiap `Text` baru meng-upload texture GPU (~1,6 detik per siklus
|
||
bila di-rebuild terus-menerus).
|
||
|
||
Suite tes: `bash panel/run_topo_tests.sh`
|
||
(`test_topo_pixels`, `test_topo_browser`, `test_topo_viewports`,
|
||
`test_topo_race`, `test_topo_drag`).
|
||
|
||
---
|
||
|
||
## HTTP API
|
||
|
||
Semua endpoint di `/api` kecuali yang ditandai publik.
|
||
|
||
### Publik
|
||
|
||
| Method | Path | Keterangan |
|
||
|---|---|---|
|
||
| GET | `/submit` | UI submit flag publik |
|
||
| POST | `/api/flag/submit` | Submit flag |
|
||
| GET | `/api/public/scoreboard` | Skorboard tanpa login |
|
||
| GET | `/api/public/teams` | Daftar tim tanpa login |
|
||
|
||
### Admin (butuh login)
|
||
|
||
| Method | Path | Keterangan |
|
||
|---|---|---|
|
||
| POST | `/api/login` | Login admin |
|
||
| POST | `/api/logout` | Logout |
|
||
| GET | `/api/challenges` | Daftar challenge + status |
|
||
| PATCH | `/api/challenges/{challenge}` | Toggle enable/disable (body `{"enabled":bool}`) |
|
||
| GET | `/api/challenges/jobs/{job_id}` | Progress job toggle |
|
||
| GET | `/api/status` | Status runtime |
|
||
| GET | `/api/topology` | Data graf topologi |
|
||
| GET | `/api/teams` | Daftar tim |
|
||
| POST | `/api/teams/set` | Buat N tim (idempoten) |
|
||
| PUT | `/api/teams/{idx}` | Ubah label/domain tim |
|
||
| DELETE | `/api/teams/{idx}` | Hapus satu tim (body `{"purge_scores":true}`) |
|
||
| POST | `/api/teams/bulk-delete` | Hapus beberapa tim (job) |
|
||
| GET | `/api/teams/bulk-delete/{job_id}` | Progress job hapus massal |
|
||
| POST | `/api/teams/{idx}/ufw` | Sinkronkan aturan UFW tim |
|
||
| POST | `/api/teams/start` | Start semua tim |
|
||
| POST | `/api/teams/stop` | Stop semua tim |
|
||
| POST | `/api/teams/{idx}/randomize` | Acak flag tim |
|
||
| GET | `/api/teams/{idx}/logs` | Log tim |
|
||
| GET | `/api/teams/{idx}/creds` | Kredensial tim |
|
||
| GET | `/api/credential/{challenge}` | Kredensial satu challenge |
|
||
| GET | `/api/targets` | Target serangan |
|
||
| GET | `/api/attacks` | Log serangan |
|
||
| GET | `/api/leaderboard` | Leaderboard |
|
||
| GET | `/api/scoreboard` | Skorboard internal |
|
||
| GET | `/api/history` | Riwayat |
|
||
| POST | `/api/restart/{challenge}` | Restart challenge |
|
||
| POST | `/api/rollback/{challenge}` | Rollback challenge |
|
||
| POST | `/api/activate/{challenge}` | Aktifkan challenge |
|
||
| POST | `/api/deactivate/{challenge}` | Nonaktifkan challenge |
|
||
| POST | `/api/reset/scores` | Reset skor |
|
||
| POST | `/api/reset/environment` | Reset environment (hapus tim + flag) |
|
||
|
||
Toggle challenge jalan asinkron: build per tim bisa memakan waktu menit, jadi
|
||
kerjaan dijalankan di background thread dan klien melakukan polling ke
|
||
`/api/challenges/jobs/{job_id}`. Hapus tim massal juga berupa job karena satu
|
||
tim butuh sekitar 100 detik (`compose down` 16 service) — empat tim inline akan
|
||
menahan request sekitar 7 menit dan memicu timeout di semua proxy.
|
||
|
||
### Tim (login tim)
|
||
|
||
| Method | Path | Keterangan |
|
||
|---|---|---|
|
||
| GET | `/team/{idx}` | Portal tim |
|
||
| GET | `/team/{idx}/guide` | Panduan tim |
|
||
| POST | `/api/team/{idx}/login` | Login tim |
|
||
| POST | `/api/team/logout` | Logout tim |
|
||
| GET | `/api/team/{idx}/session` | Status sesi |
|
||
| GET | `/api/team/{idx}/own-challenges` | Challenge milik tim |
|
||
| GET | `/api/team/{idx}/targets` | Target untuk diserang |
|
||
| GET | `/api/team/{idx}/info` | Info tim |
|
||
| GET | `/api/team/{idx}/status` | Status challenge tim |
|
||
| GET | `/api/team/{idx}/activity` | Aktivitas tim |
|
||
| WS | `/api/team/{idx}/ssh/ws` | Terminal web ke container tim |
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
**`/login` di domain attackdefense.imrnes.team mengembalikan 404.**
|
||
Domain bare diarahkan ke receiver global (:18080), bukan panel. Panel ada di
|
||
`panel.attackdefense.imrnes.team` (:18081). Dua router berbeda melayani kedua
|
||
subdomain di `attackdefense.yaml`.
|
||
|
||
**SLA turun ke 0 padahal container hidup.**
|
||
Bisa jadi receiver-nya mati, atau sering: restart panel mematikan receiver
|
||
karena keduanya satu cgroup. Cek `systemctl is-active gemastik-receiver-team*`.
|
||
|
||
**SSH ditolak padahal password di `state.json` benar.**
|
||
Cek `ssh_user` untuk challenge tersebut di registry. 10 dari 16 challenge
|
||
impor login sebagai `root`, bukan `ctfuser`.
|
||
|
||
**Flag expired / tidak cocok.**
|
||
`reset_environment()` menghapus flag lama. Cek
|
||
`teams/team<N>/receiver/flags/<challenge>.txt`.
|
||
|
||
**Waktu toggle sangat lama.**
|
||
Normal — satu toggle membangun image per tim. Pantau lewat
|
||
`/api/challenges/jobs/{job_id}`, bukan dengan kill prosesnya.
|
||
|
||
**`pull access denied for services-<name>`.**
|
||
Image belum ada sehingga compose mencoba build dengan context yang salah.
|
||
`compose_gen` hanya menukar `build` menjadi `image` bila image-nya benar-benar
|
||
ada di `docker images`.
|
||
|
||
**Disk penuh (`/` 0 byte).**
|
||
Lihat [Jebakan Operasional](#jebakan-operasional). Yang benar:
|
||
`docker builder prune -af` dan `journalctl --vacuum-size=50M`.
|
||
`docker image prune -af` menghapus image `services-*` yang sedang dipakai.
|
||
|
||
---
|
||
|
||
## Jebakan Operasional
|
||
|
||
Yang sudah ketahuan dan sudah diperbaiki. Semua masih berlaku sebagai alasan
|
||
mengapa kode sekarang berbentuk seperti sekarang.
|
||
|
||
**Base image EOL.** `debian:buster`, `ubuntu:20.04`, dan `node:14` gagal
|
||
`apt-get update` karena GPG kedaluwarsa atau mirror 404. Pakai bookworm/noble,
|
||
`node:20`.
|
||
|
||
**UFW default deny.** Host ini punya UFW aktif default deny. Port baru harus
|
||
dibuka (`ufw allow <port>/tcp`) atau traffic di-blackhole diam-diam —
|
||
termasuk dari container lewat docker bridge. `POST /api/teams/set` sudah
|
||
menjalankan `sync_team_ufw()` karena alasan ini.
|
||
|
||
**Project name compose wajib.** Per-team compose harus dijalankan dengan
|
||
`-p teamN`. Tanpa itu `docker compose` memakai nama direktori induk (`services`)
|
||
untuk semua tim, sehingga container team2 tertimpa team1.
|
||
|
||
**`docker image prune -af` menghapus image yang sedang dipakai.** Image
|
||
`services-*` menjadi dangling dan terhapus meski container masih jalan.
|
||
Container tetap hidup tetapi image hilang dan tidak bisa di-recreate. Untuk
|
||
membersihkan ruang: `docker builder prune -af` +
|
||
`journalctl --vacuum-size=100M` + hapus `/root/.cache`.
|
||
|
||
**Container orphan.** Sweep dengan:
|
||
|
||
```bash
|
||
docker ps --format '{{.Names}}' | grep -E '_container$' | grep -vE '_team[0-9]+$'
|
||
```
|
||
|
||
**Beban checker.** Host 2-CPU/8GB tidak boleh meng-probe 4 receiver
|
||
sekaligus (Flask sinkron + CPU bersama = SLA timeout palsu), dan tidak boleh
|
||
menjalankan banyak generator kunci Paillier/RSA bersamaan. Cek `uptime`,
|
||
`free -m`, `vmstat 1 3` sebelum menyalahkan checker.
|
||
|
||
**`subprocess.run(['docker','exec',...])` tanpa timeout.** Container yang
|
||
jenuh memblokir selamanya dan menahan seluruh loop SLA.
|
||
|
||
---
|
||
|
||
## Struktur Direktori
|
||
|
||
```
|
||
/opt/gemastik18-final/
|
||
├── README.md
|
||
├── node.sh # installer Docker
|
||
├── starter.py # bootstrap single-node (upstream)
|
||
├── teams/
|
||
│ ├── challenge_registry.json # sumber data tunggal 28 challenge
|
||
│ ├── points.json # skor + event
|
||
│ ├── leaderboard.json # solve
|
||
│ ├── attacks.json # log serangan (visualisasi topologi)
|
||
│ └── teamN/
|
||
│ ├── state.json # port, flag, kredensial, label, domain
|
||
│ ├── services/docker-compose.yml # hasil generate per tim
|
||
│ └── receiver/ # receiver terisolasi tim N
|
||
├── services/<challenge>/ # Dockerfile + compose template (28 challenge)
|
||
├── panel/
|
||
│ ├── main.py # FastAPI: seluruh route
|
||
│ ├── teams.py # orkestrasi tim, port, flag, skor, SLA
|
||
│ ├── compose_gen.py # render compose per tim dari registry
|
||
│ ├── gen_receiver_services.py # generate unit systemd per tim
|
||
│ ├── gen_receiver_main.py # generate main.py receiver per tim
|
||
│ ├── apply_registry.sh # sinkronkan container dengan registry
|
||
│ ├── reset_runtime.sh # reset penuh
|
||
│ ├── verify_platform_health.py
|
||
│ ├── verify_ssh_e2e.py
|
||
│ ├── audit_ssh_users.sh
|
||
│ ├── run_topo_tests.sh
|
||
│ ├── verify_topo_full.sh
|
||
│ └── static/
|
||
│ ├── index.html # dashboard admin
|
||
│ ├── team.html # portal tim
|
||
│ ├── topo_pixi.js # renderer topologi PixiJS v8
|
||
│ └── vendor/pixi.mjs
|
||
└── receiver/
|
||
├── main.py # API receiver (flag store + checker)
|
||
├── config.py
|
||
├── requirements.txt
|
||
├── challenges/ # checker: Blogpost, Phew, xvi/, xvii/
|
||
├── flags/ # flag default
|
||
└── .venv/
|
||
```
|
||
|
||
---
|
||
|
||
## Credit
|
||
|
||
Challenge berasal dari tiga repositori:
|
||
[gemastik18-final](https://github.com/rayhanhanaputra/gemastik18-final),
|
||
[gemastik-xvi-final](https://github.com/vidner/gemastik-xvi-final),
|
||
[gemastik-xvii-final](https://github.com/vidner/gemastik-xvii-final).
|