docs: full operator manual in README (setup, spec, API, troubleshooting)
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.
This commit is contained in:
@@ -1,18 +1,640 @@
|
|||||||
# Gemastik XVIII Cybersecurity Final Round - Attack Defense Repository
|
# Attack Defense Platform
|
||||||
|
|
||||||
## challenges
|
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.
|
||||||
|
|
||||||
| challenges | author | category |
|
Multi-team: N tim, masing-masing memiliki copy semua challenge + flag sendiri,
|
||||||
| ---------- | ----------- | -------- |
|
dengan receiver terisolasi per tim, checker SLA otomatis, dan visualisasi
|
||||||
| blogpost | keii | web |
|
topologi serangan real-time.
|
||||||
| cdn | keii | foren |
|
|
||||||
| phew | itoid | crypto |
|
|
||||||
| sheesh | itoid | crypto |
|
|
||||||
| carbeat | rui | pwn |
|
|
||||||
| warmup | hanz0 | warmup |
|
|
||||||
|
|
||||||
## how-to-run
|
```
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
````
|
---
|
||||||
sudo python3 starter.py
|
|
||||||
````
|
## 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).
|
||||||
|
|||||||
Reference in New Issue
Block a user