Phoenix Pipeline
Phoenix Pipeline
Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-02-05 | Status: Solved Techniques: admin_auth_bypass, file_upload_race_condition, session_puzzling, socketio_protocol, source_code_review, toctou_exploit
Summary
Task: Secure coding challenge - identify and patch two vulnerabilities in a PHP web application via a web-based IDE with Socket.IO save protocol. Solution: Fixed session puzzling (moved session assignment after validation) and file upload TOCTOU race condition (validate before move_uploaded_file).
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackthebox| ID:20260205_hackthebox_phoenix_pipeline - Tags: php, race_condition, toctou, file_upload, socketio, secure_coding, session_puzzling, auth_bypass
- Indicators: session set before validation, move_uploaded_file before mime check, predictable temp filename, Socket.IO save protocol, PHP session handling in register
- Source:
20260205_hackthebox_phoenix_pipeline.md
Foothold
Vulnerability / Misconfiguration
- Admin_auth_bypass
- File_upload_race_condition
- Session_puzzling
- Socketio_protocol
- Source_code_review
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- admin_auth_bypass
- file_upload_race_condition
- session_puzzling
- socketio_protocol
- source_code_review
- toctou_exploit
- Tags: php, race_condition, toctou, file_upload, socketio, secure_coding, session_puzzling, auth_bypass
Original Writeup
<details><summary>Click to expand original content</summary>Phoenix Pipeline — HackTheBox
Description
"With the NecroNet defeated, the Citadel unveils the Phoenix Pipeline, rebirthing global services in a resilient web — ushering in a secure, enduring tomorrow."
Two ports provided:
154.57.164.67:30603— web application (HTB Editor + PHP app)154.57.164.67:32715— connection refused (unused)
This is a secure coding challenge: identify two vulnerabilities in a PHP web application, patch them via a web-based IDE (Socket.IO save protocol), and pass the automated verifier to get the flag.
Analysis
Reconnaissance
Port 30603 served an "HTB Editor" — a React-based web IDE (Vite + Monaco Editor + Socket.IO) with a file explorer, code editor, and a "Verify" button.
The IDE exposed a REST API at /api/*:
| Endpoint | Method | Description |
|---|---|---|
/api/directory | GET | File tree listing |
/api/file?path=... | GET | Read file content |
/api/verify | GET | Check if vulnerabilities are patched, returns flag on success |
/api/create-file | POST | Create new file (409 if exists) |
Socket.IO at /socket.io for real-time file saving with messages:
{"type": "save", "data": {"fileName": "...", "content": "...", "md5": "..."}}
The /challenge/ path served the actual PHP web application ("Phoenix Pipeline - Global Infrastructure Status") with login/register, operator dashboard, admin dashboard, and reports with photo upload.
Source Code Structure
The /api/directory endpoint revealed the full PHP application:
app/
Controllers/
AuthController.php # Authentication (login/register)
OperatorController.php # Operator dashboard, report submission, file upload
AdminController.php # Admin dashboard
ApiController.php # API endpoints
Models/
Database.php # SQLite database singleton
exploit/
exploit_session_puzzling.py # PoC for vulnerability 1
exploit_file_upload.py # PoC for vulnerability 2
The GET /api/verify endpoint initially returned:
{"error": "Vulnerability 1 is not patched."}
Vulnerability 1: Session Puzzling (Admin Auth Bypass)
In AuthController::register(), the session was set BEFORE checking if the username already existed:
// VULNERABLE CODE
public static function register() {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$area = $_POST['area'] ?? '';
// Session set BEFORE existence check — BUG!
$_SESSION['username'] = $username;
$_SESSION['area'] = $area;
$stmt = $db->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
header('Location: /challenge/username-exists');
exit;
}
// ... insert user ...
}
Attack flow (confirmed by exploit_session_puzzling.py):
- POST register with
username=admin(which already exists in DB) - Session gets set:
$_SESSION['username'] = 'admin' - Duplicate check fires → redirect to
/username-exists - But session already has admin username → navigate to
/admin→ full admin access
Root cause: Session state is modified before input validation completes. The redirect does NOT destroy the session.
Vulnerability 2: File Upload Race Condition (TOCTOU)
In OperatorController::submitReport(), the uploaded file was moved to a web-accessible directory with its original extension BEFORE validating MIME type and extension:
// VULNERABLE CODE — file moved BEFORE validation
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
$tempfile = __DIR__ . '/../uploads/temp_' . $rand . '_' . $date . '.' . $ext;
// File is now on disk with original extension (e.g., .php)!
move_uploaded_file($tmp_name, $tempfile);
// Validation happens AFTER — race condition window exists
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tempfile);
if (strpos($mime, 'image/') !== 0) {
unlink($tempfile); // Too late if attacker hit the file!
// ...
}
Attack flow (confirmed by exploit_file_upload.py):
- Upload
shell.phpwith PHP webshell content - Server moves it to
/uploads/temp_{md5(filename)}_{date}.php - Simultaneously request the predictable temp path before validation completes
- PHP executes the shell → RCE
- Server eventually validates, finds non-image MIME, calls
unlink()— but too late
Root cause: Time-of-check-to-time-of-use (TOCTOU) — the file exists on disk in executable form during the validation window.
Solution
Fix 1: Session Puzzling — Move Session Assignment After Validation
Move $_SESSION assignment to AFTER the username existence check and successful database INSERT:
// FIXED CODE
public static function register() {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$area = $_POST['area'] ?? '';
// Check existence FIRST
$stmt = $db->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([$username]);
if ($stmt->fetch()) {
header('Location: /challenge/username-exists');
exit;
}
// Insert user into database
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare('INSERT INTO users (username, password, area) VALUES (?, ?, ?)');
$stmt->execute([$username, $hashedPassword, $area]);
// Session set AFTER successful registration only
$_SESSION['username'] = $username;
$_SESSION['area'] = $area;
header('Location: /challenge/operator');
exit;
}
Fix 2: File Upload Race Condition — Validate Before Moving
Validate both file extension and MIME type from the PHP temp file ($tmp_name) BEFORE calling move_uploaded_file(). Skip the intermediate temp file entirely:
// FIXED CODE — validate BEFORE moving
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
// Validate extension FIRST
if (!in_array($ext, $allowed)) {
// reject — never touches disk
return;
}
// Validate MIME from PHP temp file (not yet in web root)
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $tmp_name);
if (strpos($mime, 'image/') !== 0) {
// reject — never touches disk
return;
}
// Only move after ALL validation passes
$finalpath = __DIR__ . '/../uploads/' . $rand . '_' . $date . '.' . $ext;
move_uploaded_file($tmp_name, $finalpath);
Patching via Socket.IO
The IDE used Socket.IO for file saving with an MD5-based conflict resolution protocol:
- Read current file content via
GET /api/file?path=... - Compute MD5 of current content
- Send Socket.IO message with patched content and server's current MD5
- Server responds with
save_successorsave_conflict
Python script to apply patches:
#!/usr/bin/env python3
"""
Patch both vulnerabilities via Socket.IO save protocol.
"""
import hashlib
import requests
import socketio
BASE = "http://154.57.164.67:30603"
def get_file(path):
r = requests.get(f"{BASE}/api/file", params={"path": path})
return r.json()["content"]
def md5(content):
return hashlib.md5(content.encode()).hexdigest()
# Read current files
auth_path = "app/Controllers/AuthController.php"
operator_path = "app/Controllers/OperatorController.php"
auth_content = get_file(auth_path)
operator_content = get_file(operator_path)
auth_md5 = md5(auth_content)
operator_md5 = md5(operator_content)
# Prepare patched content (replace vulnerable sections)
patched_auth = auth_content # ... apply session puzzling fix
patched_operator = operator_content # ... apply TOCTOU fix
# Connect via Socket.IO and save
sio = socketio.Client()
@sio.on("connect")
def on_connect():
sio.emit("message", {
"type": "save",
"data": {
"fileName": auth_path,
"content": patched_auth,
"md5": auth_md5
}
})
@sio.on("save_success")
def on_save_success(data):
if data.get("fileName") == auth_path:
sio.emit("message", {
"type": "save",
"data": {
"fileName": operator_path,
"content": patched_operator,
"md5": operator_md5
}
})
else:
# Both saved, verify
r = requests.get(f"{BASE}/api/verify")
print(r.json())
sio.disconnect()
sio.connect(BASE, transports=["websocket"])
sio.wait()
Both saves succeeded, then GET /api/verify returned the flag.
Defense (Best Practices)
Session Management
- Never set session before validation — session state should only change after ALL checks pass
- Destroy session on failed registration —
session_destroy()if validation fails - Use RBAC — don't rely on username matching for admin access
File Upload
- Validate before moving — check extension + MIME from
$_FILES['tmp_name'](not web-accessible) - Whitelist extensions —
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'] - Rename uploaded files — never preserve original filename/extension
- Store outside web root — serve via a controller, not direct access
- No intermediate temp files in web root — eliminates the race window entirely
Tools
- curl — HTTP reconnaissance, API interaction
- Python requests — API interaction, file reading
- python-socketio — Socket.IO client for file saving protocol
- Browser DevTools — JS bundle analysis, understanding frontend architecture (Vite + React + Monaco Editor)
Notes
This challenge combines two distinct vulnerability classes into a single "secure coding" task:
-
Session Puzzling is a lesser-known session management vulnerability. Unlike session fixation (attacker sets session ID) or session hijacking (attacker steals session ID), session puzzling exploits the application's own logic setting session variables at the wrong time. The key insight is that
header('Location: ...')in PHP sends a redirect but does NOT terminate the session — the session data persists. -
TOCTOU in file uploads is a classic race condition. The window between
move_uploaded_file()andunlink()may be tiny, but with concurrent requests it's reliably exploitable. The fix is simple: never let untrusted content touch the web root until fully validated.
The Socket.IO save protocol with MD5 conflict resolution added an interesting twist — you couldn't just POST the fix, you had to understand and implement the real-time save protocol.
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR