2026-09-23 16:18:11 +08:00
<!DOCTYPE html>
< html lang = "id" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-09-23 16:37:58 +08:00
< meta name = "team-id" content = "0" >
< title > Portal Tim — Gemastik A/D</ title >
< 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 >
2026-09-23 16:18:11 +08:00
< style >
2026-09-23 16:37:58 +08:00
: root { --bg : #0b0e14 ; --panel : #151a23 ; --panel2 : #1d2430 ; --line : #2a3444 ; --txt : #e6edf3 ; --dim : #8b98a9 ; --accent : #38bdf8 ; --green : #34d399 ; --red : #f87171 ; --purple : #a78bfa ; }
2026-09-23 16:18:11 +08:00
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body { background : var ( -- bg ); color : var ( -- txt ); font-family : 'Segoe UI' , system-ui , sans-serif ; min-height : 100 vh ; }
2026-09-23 16:37:58 +08:00
. wrap { max-width : 1000 px ; margin : 0 auto ; padding : 24 px 16 px 60 px ; }
2026-09-23 16:18:11 +08:00
header { display : flex ; align-items : center ; gap : 12 px ; margin-bottom : 24 px ; flex-wrap : wrap ; }
. logo { width : 42 px ; height : 42 px ; border-radius : 10 px ; background : linear-gradient ( 135 deg , #38bdf8 , #6366f1 ); display : flex ; align-items : center ; justify-content : center ; font-weight : 800 ; font-size : 20 px ; color : #fff ; }
h1 { font-size : 22 px ; }
. sub { color : var ( -- dim ); font-size : 13 px ; }
. badge { background : #06352a ; border : 1 px solid var ( -- green ); color : var ( -- green ); border-radius : 20 px ; padding : 3 px 12 px ; font-size : 12 px ; font-weight : 600 ; }
2026-09-23 16:37:58 +08:00
. badge . stopped { background : #3a1414 ; border-color : var ( -- red ); color : var ( -- red ); }
2026-09-23 16:18:11 +08:00
. card { background : var ( -- panel ); border : 1 px solid var ( -- line ); border-radius : 14 px ; padding : 20 px ; margin-bottom : 16 px ; }
. card h2 { font-size : 15 px ; margin-bottom : 12 px ; color : var ( -- accent ); }
. kv { display : grid ; grid-template-columns : 150 px 1 fr ; gap : 6 px 12 px ; font-size : 13 px ; margin-bottom : 4 px ; }
. kv b { color : var ( -- dim ); font-weight : 500 ; }
. kv span { font-family : ui-monospace , monospace ; }
table { width : 100 % ; border-collapse : collapse ; font-size : 13 px ; }
th , td { text-align : left ; padding : 8 px 6 px ; border-bottom : 1 px solid var ( -- line ); }
th { color : var ( -- dim ); font-weight : 500 ; }
. dot { display : inline-block ; width : 8 px ; height : 8 px ; border-radius : 50 % ; margin-right : 6 px ; }
. dot . up { background : var ( -- green ); } . dot . down { background : var ( -- red ); }
label { display : block ; font-size : 12 px ; color : var ( -- dim ); margin : 12 px 0 5 px ; }
2026-09-23 16:37:58 +08:00
select , input [ type = text ], input [ type = password ] { width : 100 % ; padding : 10 px 12 px ; border-radius : 8 px ; border : 1 px solid var ( -- line ); background : var ( -- panel2 ); color : var ( -- txt ); font-size : 14 px ; }
button { padding : 12 px 16 px ; border : none ; border-radius : 8 px ; background : linear-gradient ( 135 deg , #38bdf8 , #6366f1 ); color : #fff ; font-weight : 600 ; font-size : 14 px ; cursor : pointer ; }
2026-09-23 16:18:11 +08:00
button : hover { filter : brightness ( 1.1 ); }
2026-09-23 16:37:58 +08:00
button . ghost { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); color : var ( -- txt ); }
2026-09-23 16:18:11 +08:00
# result { margin-top : 14 px ; padding : 12 px ; border-radius : 8 px ; display : none ; font-size : 14 px ; }
# result . ok { display : block ; background : #06352a ; border : 1 px solid var ( -- green ); color : var ( -- green ); }
# result . err { display : block ; background : #3a1414 ; border : 1 px solid var ( -- red ); color : var ( -- red ); }
. sshbox { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); border-radius : 8 px ; padding : 10 px 12 px ; font-family : ui-monospace , monospace ; font-size : 12 px ; overflow-x : auto ; }
2026-09-23 16:37:58 +08:00
/* login */
# loginScreen { max-width : 420 px ; margin : 80 px auto ; }
# loginScreen . card { padding : 32 px ; }
# loginScreen h2 { text-align : center ; margin-bottom : 6 px ; }
# loginScreen . hint { text-align : center ; color : var ( -- dim ); font-size : 12 px ; margin-bottom : 18 px ; }
. login-err { color : var ( -- red ); font-size : 13 px ; margin-top : 10 px ; display : none ; }
/* tabs */
. tabs { display : flex ; gap : 8 px ; margin-bottom : 16 px ; flex-wrap : wrap ; }
. tabs button { background : var ( -- panel2 ); border : 1 px solid var ( -- line ); color : var ( -- dim ); font-weight : 500 ; }
. tabs button . active { background : linear-gradient ( 135 deg , #38bdf8 , #6366f1 ); color : #fff ; border-color : transparent ; }
. view { display : none ; }
. view . active { display : block ; }
/* terminal */
# termWrap { background : #0d1117 ; border : 1 px solid var ( -- line ); border-radius : 10 px ; padding : 8 px ; height : 480 px ; }
# term { height : 100 % ; }
. term-toolbar { display : flex ; gap : 8 px ; align-items : center ; margin-bottom : 10 px ; flex-wrap : wrap ; }
# termStatus { font-size : 12 px ; color : var ( -- dim ); }
code { background : var ( -- panel2 ); padding : 2 px 6 px ; border-radius : 5 px ; font-size : 12 px ; }
. guide h3 { color : var ( -- purple ); margin : 18 px 0 8 px ; font-size : 14 px ; }
. guide p , . guide li { font-size : 13 px ; color : #c9d4e3 ; line-height : 1.7 ; }
. guide ol , . guide ul { padding-left : 20 px ; }
. login-note { font-size : 12 px ; color : var ( -- dim ); margin-top : 14 px ; text-align : center ; }
. login-note a { color : var ( -- accent ); }
2026-09-23 16:18:11 +08:00
</ style >
</ head >
< body >
< div class = "wrap" >
< header >
< div class = "logo" > G</ div >
< div style = "flex:1" >
< h1 id = "teamTitle" > Portal Tim</ h1 >
< div class = "sub" id = "teamDomain" ></ div >
</ div >
< span class = "badge" id = "teamStatus" > …</ span >
2026-09-23 16:37:58 +08:00
< button class = "ghost" id = "logoutBtn" style = "display:none" onclick = "logout()" > Keluar</ button >
2026-09-23 16:18:11 +08:00
</ header >
2026-09-23 16:37:58 +08:00
<!-- LOGIN -->
< div id = "loginScreen" >
< div class = "card" >
< h2 > 🔐 Login Portal Tim</ h2 >
< div class = "hint" id = "loginHint" > Masukkan < b > password SSH tim</ b > kamu (dari panitia)</ div >
< label > Password Tim</ label >
< input type = "password" id = "loginPass" placeholder = "password SSH tim" autocomplete = "off" >
< button onclick = "doLogin()" style = "width:100%;margin-top:16px" > Masuk Portal</ button >
< div class = "login-err" id = "loginErr" ></ div >
2026-09-23 17:02:52 +08:00
< div class = "login-note" > Belum punya password? Hubungi panitia.< br > Tutorial SSH: < a href = "/team/0/guide" data-guide-link > baca panduan</ a ></ div >
2026-09-23 16:18:11 +08:00
</ div >
</ div >
2026-09-23 16:37:58 +08:00
<!-- PORTAL -->
< div id = "portalScreen" style = "display:none" >
< div class = "tabs" >
< button class = "active" data-view = "term" onclick = "showView('term')" > 🖥️ Terminal SSH</ button >
< button data-view = "targets" onclick = "showView('targets')" > 🎯 Target Musuh</ button >
< button data-view = "submit" onclick = "showView('submit')" > 🚩 Submit Flag</ button >
< button data-view = "guide" onclick = "showView('guide')" > 📖 Panduan SSH</ button >
</ div >
2026-09-23 16:18:11 +08:00
2026-09-23 16:37:58 +08:00
< div class = "view active" id = "view-term" >
< div class = "card" >
< h2 > 🖥️ SSH Terminal — pilih challenge</ h2 >
< div class = "term-toolbar" >
< select id = "termChall" style = "width:200px" onchange = "connectTerm()" >
< option value = "blogpost" > blogpost (web)</ option >
< option value = "carbeat" > carbeat (pwn)</ option >
< option value = "cdn" > cdn (web)</ option >
< option value = "phew" > phew (crypto)</ option >
< option value = "sheesh" > sheesh (crypto)</ option >
< option value = "warmup" > warmup</ option >
</ select >
< button class = "ghost" onclick = "connectTerm()" > 🔄 Sambung</ button >
< span id = "termStatus" > Belum tersambung</ span >
</ div >
< div id = "termWrap" >< div id = "term" ></ div ></ div >
< div style = "margin-top:10px;font-size:12px;color:var(--dim)" >
SSH otomatis login sebagai < code > ctfuser</ code > ke container challenge timmu. Koneksi diputus setelah idle.
</ div >
</ div >
</ div >
< div class = "view" id = "view-targets" >
< div class = "card" >
< h2 > 🎯 Target Musuh</ h2 >
< p style = "font-size:13px;color:var(--dim);margin-bottom:12px" > Service challenge tim lain — inilah yang harus kamu attack & curi flagnya.</ p >
< div id = "targetList" class = "sshbox" > Memuat…</ div >
</ div >
</ div >
< div class = "view" id = "view-submit" >
< div class = "card" >
< h2 > 🚩 Submit Flag</ h2 >
< label > Challenge</ label >
< select id = "challenge" >
< option value = "blogpost" > blogpost (web)</ option >
< option value = "carbeat" > carbeat (pwn)</ option >
< option value = "cdn" > cdn (web)</ option >
< option value = "phew" > phew (crypto)</ option >
< option value = "sheesh" > sheesh (crypto)</ option >
< option value = "warmup" > warmup</ option >
</ select >
< label > Flag</ label >
< input type = "text" id = "flag" placeholder = "GEMASTIK18{...}" autocomplete = "off" >
< button onclick = "submitFlag()" style = "width:100%;margin-top:16px" > Submit Flag</ button >
< div id = "result" ></ div >
</ div >
</ div >
< div class = "view" id = "view-guide" >
< div class = "card guide" >
< h2 > 📖 Panduan SSH & Attack</ h2 >
< h3 > 1. Apa itu SSH di sini?</ h3 >
< p > Setiap challenge punya service yang harus kamu < b > pertahankan</ b > (defense) dan service tim lain yang harus kamu < b > serang</ b > (attack). SSH container adalah akses administratif ke challenge tim kamu — untuk riset, patch, atau recovery.</ p >
< h3 > 2. Login via Web Terminal</ h3 >
< ol >
< li > Buka tab < b > 🖥️ Terminal SSH</ b > .</ li >
< li > Pilih challenge (misal < code > blogpost</ code > ).</ li >
< li > Klik < b > Sambung</ b > — otomatis login sebagai < code > ctfuser</ code > .</ li >
</ ol >
< h3 > 3. Login via SSH biasa (opsional)</ h3 >
< div class = "sshbox" > ssh ctfuser@43.134.105.109 -p < PORT_SSH>
# password = password tim kamu</ div >
< h3 > 4. Command yang berguna</ h3 >
< ul >
< li >< code > cat /flag.txt</ code > — flag < b > tim kamu sendiri</ b > (jangan bocorin!)</ li >
< li >< code > ls /app /srv /opt</ code > — cari source code challenge</ li >
< li >< code > ps aux</ code > — lihat proses service</ li >
< li >< code > cat /flag.txt | nc < ip_tim_lawan> < port_chall> </ code > — kirim flag kamu 🤫</ li >
</ ul >
< h3 > 5. Alur Attack</ h3 >
< ol >
< li > Cari vuln di challenge (SSTI, path traversal, crypto oracle, dll).</ li >
< li > Exploit service < b > tim lawan</ b > — lihat tab < b > 🎯 Target Musuh</ b > .</ li >
< li > Baca flag dari container/service lawan.</ li >
< li > Submit flag di tab < b > 🚩 Submit Flag</ b > → dapat poin.</ li >
</ ol >
< h3 > 6. Aturan</ h3 >
< ul >
< li > Jangan ubah flag tim sendiri (mount read-only).</ li >
< li > Jangan matikan service tim sendiri — SLA dicek panitia.</ li >
< li > Flag yang valid: < code > GEMASTIK18{...}</ code > .</ li >
</ ul >
</ div >
</ div >
2026-09-23 16:18:11 +08:00
</ div >
</ div >
< script >
const TEAM_ID = parseInt ( document . querySelector ( 'meta[name="team-id"]' ). content || '0' , 10 );
2026-09-23 16:37:58 +08:00
let term = null , ws = null ;
2026-09-23 16:18:11 +08:00
2026-09-23 17:02:52 +08:00
// fix all guide links immediately (no auth needed) — /team/0 would 403
document . querySelectorAll ( '[data-guide-link]' ). forEach ( a => a . href = `/team/ ${ TEAM_ID } /guide` );
2026-09-23 16:18:11 +08:00
async function api ( url , opts ) { const r = await fetch ( url , opts ); return r . json (); }
2026-09-23 16:37:58 +08:00
function esc ( s ) { return String ( s ?? '' ). replace ( /[&<>"']/g , c => ({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' }[ c ])); }
function showView ( v ) {
document . querySelectorAll ( '.tabs button' ). forEach ( b => b . classList . toggle ( 'active' , b . dataset . view === v ));
document . querySelectorAll ( '.view' ). forEach ( x => x . classList . toggle ( 'active' , x . id === 'view-' + v ));
if ( v === 'term' && term ) term . focus ();
if ( v === 'targets' ) loadTargets ();
}
2026-09-23 16:18:11 +08:00
2026-09-23 16:37:58 +08:00
// ---------- auth ----------
async function checkSession () {
const s = await api ( `/api/team/ ${ TEAM_ID } /session` );
if ( s . authed ) {
document . getElementById ( 'loginScreen' ). style . display = 'none' ;
document . getElementById ( 'portalScreen' ). style . display = 'block' ;
document . getElementById ( 'logoutBtn' ). style . display = '' ;
loadInfo ();
2026-09-23 16:18:11 +08:00
}
}
2026-09-23 16:37:58 +08:00
async function doLogin () {
const pass = document . getElementById ( 'loginPass' ). value ;
const err = document . getElementById ( 'loginErr' );
err . style . display = 'none' ;
const d = await api ( `/api/team/ ${ TEAM_ID } /login` , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({ pass }),
});
if ( d . ok ) {
document . getElementById ( 'loginScreen' ). style . display = 'none' ;
document . getElementById ( 'portalScreen' ). style . display = 'block' ;
document . getElementById ( 'logoutBtn' ). style . display = '' ;
loadInfo ();
} else {
err . textContent = 'Password salah. Coba lagi.' ;
err . style . display = 'block' ;
}
}
async function logout () {
await api ( '/api/team/logout' , { method : 'POST' });
location . reload ();
}
// ---------- info & targets ----------
async function loadInfo () {
try {
const t = ( await api ( `/api/team/ ${ TEAM_ID } /info` )). team ;
document . getElementById ( 'teamTitle' ). textContent = t . label || ( 'Team ' + TEAM_ID );
document . getElementById ( 'teamDomain' ). textContent = t . domain || '' ;
const st = document . getElementById ( 'teamStatus' );
if ( t . status === 'running' ) {
st . textContent = '● RUNNING' ; st . className = 'badge' ;
} else {
st . textContent = '○ STOPPED' ; st . className = 'badge stopped' ;
}
document . getElementById ( 'loginHint' ). innerHTML = 'Masukkan <b>password SSH tim</b> kamu (dari panitia)' ;
document . getElementById ( 'guideLink' ). href = `/team/ ${ TEAM_ID } /guide` ;
} catch ( e ) {}
}
async function loadTargets () {
const box = document . getElementById ( 'targetList' );
const d = await api ( `/api/team/ ${ TEAM_ID } /targets` );
const targets = d . targets || [];
if ( ! targets . length ) { box . textContent = 'Belum ada tim musuh.' ; return ; }
let html = '=== TARGET MUSUH ===\n\n' ;
for ( const t of targets ) {
html += `[ ${ esc ( t . team ) } ] — ${ esc ( t . challenge ) } \n` ;
if ( t . domain ) html += ` domain : https:// ${ esc ( t . domain ) } \n` ;
html += ` service: ${ esc ( window . location . hostname ) } : ${ t . port } \n` ;
html += ` ssh : ${ esc ( window . location . hostname ) } : ${ t . ssh } \n\n` ;
}
box . textContent = html ;
}
// ---------- SSH terminal ----------
function connectTerm () {
const chall = document . getElementById ( 'termChall' ). value ;
const status = document . getElementById ( 'termStatus' );
const proto = location . protocol === 'https:' ? 'wss:' : 'ws:' ;
const url = ` ${ proto } // ${ location . host } /api/team/ ${ TEAM_ID } /ssh/ws?chall= ${ chall } ` ;
if ( ws ) { try { ws . close (); } catch ( e ){} }
if ( term ) term . dispose ();
term = new Terminal ({ cursorBlink : true , fontSize : 13 , theme : { background : '#0d1117' }});
term . open ( document . getElementById ( 'term' ));
ws = new WebSocket ( url );
status . textContent = 'Menghubungkan…' ;
ws . onopen = () => { status . textContent = `● tersambung ke ${ chall } (ctfuser)` ; term . focus (); };
ws . onmessage = ev => term . write ( ev . data );
ws . onclose = ev => { status . textContent = '✖ terputus (' + ( ev . reason || 'closed' ) + ')' ; };
ws . onerror = () => { status . textContent = '✖ error koneksi' ; };
term . onData ( d => {
if ( ws && ws . readyState === WebSocket . OPEN ) ws . send ( d );
});
term . onResize ( size => {
if ( ws && ws . readyState === WebSocket . OPEN ) ws . send ( `__resize__: ${ size . cols } : ${ size . rows } ` );
});
document . getElementById ( 'view-term' ). querySelector ( '#termWrap' ). style . height = '480px' ;
}
// ---------- submit ----------
2026-09-23 16:18:11 +08:00
async function submitFlag () {
const res = document . getElementById ( 'result' );
res . className = '' ;
const d = await api ( '/api/flag/submit' , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' },
body : JSON . stringify ({
team : TEAM_ID ,
challenge : document . getElementById ( 'challenge' ). value ,
flag : document . getElementById ( 'flag' ). value . trim (),
}),
});
2026-09-23 16:37:58 +08:00
if ( d . success ) { res . className = 'ok' ; res . textContent = '✅ Flag benar! Poin masuk.' ; }
else { res . className = 'err' ; res . textContent = '❌ ' + ( d . error || 'Gagal' ); }
2026-09-23 16:18:11 +08:00
}
2026-09-23 16:37:58 +08:00
checkSession ();
2026-09-23 16:18:11 +08:00
</ script >
</ body >
</ html >