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 @@