diff --git a/panel/main.py b/panel/main.py index 5a69c37..c043fea 100644 --- a/panel/main.py +++ b/panel/main.py @@ -88,6 +88,7 @@ async def index(req: Request): if t.get("slug") == slug: html = (BASE_DIR / "static" / "team.html").read_text() html = html.replace('name="team-id" content="0"', f'name="team-id" content="{t["index"]}"') + html = html.replace('href="/team/0/guide"', f'href="/team/{t["index"]}/guide"') return HTMLResponse(html) return HTMLResponse("

Team tidak ditemukan: " + slug + "

", status_code=404) if not _authorized(req): @@ -116,14 +117,18 @@ async def team_portal(idx: int, req: Request): if not (td / "state.json").exists(): raise HTTPException(404, "Team not found") st = json.loads((td / "state.json").read_text()) - host = (req.headers.get("host") or "").split(":")[0] - # host check: allow panel domain (uses explicit /team/N link for admin preview) - # and the team's own .domain; block cross-team access. - if not (host == f"panel.gemastik.imrnes.team" or host == f"gemastik.imrnes.team" - or host == st.get("domain") or host.startswith("127.0.0.1") or host.startswith("localhost")): + if not _check_team_host(req, st): raise HTTPException(403, "Akses team lain tidak diizinkan") + if _team_authorized(req, idx): # logged in -> show portal directly + html = (BASE_DIR / "static" / "team.html").read_text() + html = html.replace('name="team-id" content="0"', f'name="team-id" content="{idx}"') + # server-side: fix guide link so non-JS / pre-JS clicks never hit /team/0 + html = html.replace('href="/team/0/guide"', f'href="/team/{idx}/guide"') + return HTMLResponse(html) + # not logged in -> show a stripped landing/login page (no admin info) html = (BASE_DIR / "static" / "team.html").read_text() html = html.replace('name="team-id" content="0"', f'name="team-id" content="{idx}"') + html = html.replace('href="/team/0/guide"', f'href="/team/{idx}/guide"') return HTMLResponse(html) @@ -131,10 +136,10 @@ async def team_portal(idx: int, req: Request): async def team_guide(idx: int, req: Request): """Public SSH/attack guide for a team. Must be accessed via that team's own domain.""" td = orch.TEAMS_DIR / f"team{idx}" - st = json.loads((td / "state.json").read_text()) if (td / "state.json").exists() else {} - host = (req.headers.get("host") or "").split(":")[0] - if not (host == f"panel.gemastik.imrnes.team" or host == f"gemastik.imrnes.team" - or host == st.get("domain") or host.startswith("127.0.0.1") or host.startswith("localhost")): + if not (td / "state.json").exists(): + raise HTTPException(404, "Team not found") + st = json.loads((td / "state.json").read_text()) + if not _check_team_host(req, st): raise HTTPException(403, "Akses team lain tidak diizinkan") html = (BASE_DIR / "static" / "guide.html").read_text() html = html.replace('name="team-id" content="0"', f'name="team-id" content="{idx}"') @@ -149,6 +154,8 @@ async def api_team_login(idx: int, req: Request): if not (td / "state.json").exists(): raise HTTPException(404, "Team not found") st = json.loads((td / "state.json").read_text()) + if not _check_team_host(req, st): + raise HTTPException(403, "Akses team lain tidak diizinkan") data = await req.json() pw = data.get("pass", "") if pw != st.get("ssh_pass"): @@ -176,6 +183,18 @@ def _team_authorized(req: Request, idx: int) -> bool: return False return True +def _check_team_host(req: Request, st: dict) -> bool: + """IDOR guard: host must be this team's own domain (or localhost). + panel.gemastik / gemastik.imrnes.team only allowed with a valid ADMIN session.""" + host = (req.headers.get("host") or "").split(":")[0] + if host == st.get("domain"): + return True + if host.startswith("127.0.0.1") or host.startswith("localhost"): + return True + if host in ("panel.gemastik.imrnes.team", "gemastik.imrnes.team"): + return _authorized(req) # admin preview only + return False + @app.get("/api/team/{idx}/session") async def api_team_session(idx: int, req: Request): """True when this browser has a valid team session for idx.""" @@ -183,7 +202,15 @@ async def api_team_session(idx: int, req: Request): @app.get("/api/team/{idx}/targets") async def api_team_targets(idx: int, req: Request): - """Public: list of enemy teams' services (attack targets) for this team's portal.""" + """Team targets — requires team login + own host.""" + td = orch.TEAMS_DIR / f"team{idx}" + if not (td / "state.json").exists(): + raise HTTPException(404, "Team not found") + st_self = json.loads((td / "state.json").read_text()) + if not _check_team_host(req, st_self): + raise HTTPException(403, "Akses team lain tidak diizinkan") + if not _team_authorized(req, idx): + raise HTTPException(401, "Login portal team dulu") out = [] for d in sorted(orch.TEAMS_DIR.glob("team*")): if not (d / "state.json").exists(): @@ -207,13 +234,15 @@ async def api_team_targets(idx: int, req: Request): @app.get("/api/team/{idx}/info") async def api_team_info(idx: int, req: Request): - """Team portal info — requires team login; no admin secrets.""" - if not _team_authorized(req, idx): - raise HTTPException(401, "Login portal team dulu") + """Team portal info — requires team login + own host; no admin secrets.""" td = orch.TEAMS_DIR / f"team{idx}" if not (td / "state.json").exists(): raise HTTPException(404, "Team not found") st = json.loads((td / "state.json").read_text()) + if not _check_team_host(req, st): + raise HTTPException(403, "Akses team lain tidak diizinkan") + if not _team_authorized(req, idx): + raise HTTPException(401, "Login portal team dulu") return {"team": { "index": st.get("index"), "label": st.get("label"), @@ -227,12 +256,14 @@ async def api_team_info(idx: int, req: Request): @app.get("/api/team/{idx}/status") -async def api_team_status(idx: int): - """Public: SLA status for a team's challenges (no auth — read-only health).""" +async def api_team_status(idx: int, req: Request): + """SLA status for a team's challenges (read-only health, own-host only).""" td = orch.TEAMS_DIR / f"team{idx}" if not (td / "state.json").exists(): raise HTTPException(404, "Team not found") st = json.loads ((td / "state.json").read_text()) + if not _check_team_host(req, st): + raise HTTPException(403, "Akses team lain tidak diizinkan") recv_port = st["ports"]["receiver"] # use team's receiver admin creds (server-side only; never sent to browser) au, ap = st.get("admin_user", ""), st.get("admin_pass", "") @@ -534,6 +565,11 @@ async def team_ssh_ws(ws: WebSocket, idx: int): return td = orch.TEAMS_DIR / f"team{idx}" st = json.loads((td / "state.json").read_text()) + # host check (IDOR): only this team's domain can open its SSH + host = (ws.headers.get("host") or "").split(":")[0] + if not (host == st.get("domain") or host.startswith("127.0.0.1") or host.startswith("localhost")): + await ws.close(code=4003, reason="wrong host") + return if chall not in st.get("ports", {}): await ws.close(code=4002, reason="unknown challenge") return diff --git a/panel/static/guide.html b/panel/static/guide.html index 86123b7..989c5f0 100644 --- a/panel/static/guide.html +++ b/panel/static/guide.html @@ -96,13 +96,13 @@

📞 Butuh Bantuan?

Hubungi panitia untuk: reset password SSH, restart container, atau lapor service down. Portal ini untuk peserta — panel admin terpisah.

-

← Kembali ke portal tim

+

← Kembali ke portal tim

\ No newline at end of file diff --git a/panel/static/index.html b/panel/static/index.html index 05e7759..fa72225 100644 --- a/panel/static/index.html +++ b/panel/static/index.html @@ -83,6 +83,15 @@ } .toast.show { opacity:1; transform:translateY(0); } .toast.err { border-color:#f87171; color:#fca5a5; } + /* loading overlay */ + #loadingOverlay { + display:none; position:fixed; inset:0; background:rgba(4,8,14,0.82); z-index:2000; + align-items:center; justify-content:center; flex-direction:column; gap:18px; + } + #loadingOverlay.show { display:flex; } + .loader { width:44px; height:44px; border:4px solid #2a3444; border-top-color:#38bdf8; border-radius:50%; animation:spin 0.9s linear infinite; } + @keyframes spin { to { transform:rotate(360deg); } } + #loadingText { color:#dbe6f4; font-size:14px; text-align:center; padding:0 24px; line-height:1.6; } .history-box { background:#0a101f; border:1px solid #1e3a5f; border-radius:10px; padding:14px; font-size:11px; color:#7dd3fc; max-height:300px; overflow-y:auto; white-space:pre-wrap; @@ -219,6 +228,12 @@
+ +
+
+
Memproses…
+
+