← Back to Writeups
HTBN/AWeb

Phoenix Pipeline

XESXOR8/23/20268 min read
#web#htb#n/a

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

  1. Admin_auth_bypass
  2. File_upload_race_condition
  3. Session_puzzling
  4. Socketio_protocol
  5. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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/*:

EndpointMethodDescription
/api/directoryGETFile tree listing
/api/file?path=...GETRead file content
/api/verifyGETCheck if vulnerabilities are patched, returns flag on success
/api/create-filePOSTCreate 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):

  1. POST register with username=admin (which already exists in DB)
  2. Session gets set: $_SESSION['username'] = 'admin'
  3. Duplicate check fires → redirect to /username-exists
  4. 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):

  1. Upload shell.php with PHP webshell content
  2. Server moves it to /uploads/temp_{md5(filename)}_{date}.php
  3. Simultaneously request the predictable temp path before validation completes
  4. PHP executes the shell → RCE
  5. 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:

  1. Read current file content via GET /api/file?path=...
  2. Compute MD5 of current content
  3. Send Socket.IO message with patched content and server's current MD5
  4. Server responds with save_success or save_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

  1. Never set session before validation — session state should only change after ALL checks pass
  2. Destroy session on failed registrationsession_destroy() if validation fails
  3. Use RBAC — don't rely on username matching for admin access

File Upload

  1. Validate before moving — check extension + MIME from $_FILES['tmp_name'] (not web-accessible)
  2. Whitelist extensions['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
  3. Rename uploaded files — never preserve original filename/extension
  4. Store outside web root — serve via a controller, not direct access
  5. 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:

  1. 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.

  2. TOCTOU in file uploads is a classic race condition. The window between move_uploaded_file() and unlink() 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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR