← Back to list

Four-Way PHP Domino-Fall: Unchecked Input to Full Root Compromise

A single, seemingly innocent HTTP endpoint can form a critical business-impact chain when multiple structural PHP weaknesses are stitched…

Xia0checkmate · 2026-05-29 17:39 · 50 claps · 2.9 min read
#php #bug-bounty-writeup #source-code-review #rce-vulnerability #critical-thinking
Open on Medium ↗
Wiki topics: FT · Fine-tuning & Adaptation HUM · Humanities · General 🔒 · Cybersecurity

Four-Way PHP Domino-Fall: Unchecked Input to Full Root Compromise

A single, seemingly innocent HTTP endpoint can form a critical business-impact chain when multiple structural PHP weaknesses are stitched together. This write-up details a four-stage architectural exploit chain — progressing from unchecked parameters to complete server takeover — followed by a production-ready defensive refactor.

## The Exploitation Chain Architecture

The vulnerability cascade relies on a strict sequential dependency. Breaking any single link collapses the entire attack surface.

[Unsanitized GET/POST] ──> [Remote File Inclusion] ──> [OS Command Injection] ──> [Privilege Escalation (Root)]

1. Arbitrary Local/Remote File Inclusion (RFI)

The application handles file routing through a raw parameter concatenation without strict allow-listing or input sanitization:

// public/download.php
include $_GET['template'];

An attacker inputs an external URL pointing to a controlled domain hosting a malicious payload. If allow_url_include is enabled in the environment, the remote runtime executes within the target application context.

2. Contextual OS Command Injection

Once code execution is achieved via the inclusion vector, the runtime evaluates a secondary script (system/exec.php) designed to interface with binary utilities like ffmpeg:

// system/exec.php
system("ffmpeg -i " . $_GET['src'] . " " . $_GET['out']);

Because the input parameters bypass command-argument escaping wrappers, the shell appends secondary commands, executing arbitrary OS utilities under the privileges of the web-server user.

3. Privilege Escalation to Root

The web-server daemon operates as www-data but possesses overly permissive write access to critical execution paths such as /usr/local/bin/. By abusing the command injection flaw, the attacker forces the system to write a set-uid binary (pwned), assigning root execution rights upon invocation.

Business Impact Summary

  • Data Exfiltration: Unrestricted access to the file-system allows a full database dump, including PII, financial ledgers, and private API keys.
  • Compliance Failure: Neglecting basic input separation violates OWASP ASVS Level 2 controls, shifting the liability to technical negligence.

Defensive Refactor (PHP 8.2+, PSR-4, Strict Typing)

To neutralize this attack vector, the logic must enforce strict type-casting, exact pattern matching, and absolute directory confinement using realpath.

The Gateway Controller

<?php
declare(strict_types=1);

namespace App\Controller;

use App\Service\TemplateRenderer;
use App\Util\Logger;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\Response\HtmlResponse;

final class DownloadController
{
    private TemplateRenderer $renderer;
    private Logger $log;

    public function __construct(TemplateRenderer $renderer, Logger $log)
    {
        $this->renderer = $renderer;
        $this->log = $log;
    }

    public function __invoke(ServerRequestInterface $request): ResponseInterface
    {
        $query = $request->getQueryParams();
        $template = $query['template'] ?? '';

        // Enforce strict alphanumeric whitelist evaluation
        if (!preg_match('/^[a-z0-9_-]+$/i', $template)) {
            $this->log->warning('Malformed template parameter blocked', ['template' => $template]);
            return new JsonResponse(['error' => 'Invalid resource path'], 400);
        }

        $baseDir = __DIR__ . '/../templates/';
        $filePath = realpath($baseDir . $template . '.php');

        // Prevent path traversal and directory breakouts
        if ($filePath === false || strpos($filePath, $baseDir) !== 0) {
            $this->log->warning('Directory traversal attempt mitigated', ['template' => $template]);
            return new JsonResponse(['error' => 'Resource not found'], 404);
        }

        try {
            $output = $this->renderer->render($filePath, $query);
        } catch (\Throwable $e) {
            $this->log->error('Render subsystem exception', ['exception' => $e]);
            return new JsonResponse(['error' => 'Internal server execution error'], 500);
        }

        $response = new HtmlResponse($output);
        return $response->withHeader(
            'Content-Security-Policy',
            "default-src 'self'; script-src 'self'; object-src 'none';"
        );
    }
}

The Isolated Template Renderer

 <?php
declare(strict_types=1);

namespace App\Service;

use Psr\Log\LoggerInterface;

final class TemplateRenderer
{
    private LoggerInterface $log;

    public function __construct(LoggerInterface $log)
    {
        $this->log = $log;
    }

    public function render(string $filePath, array $variables): string
    {
        $safeVars = $this->sanitizeVariables($variables);

        // Establish clean execution boundaries via output buffering
        ob_start();
        try {
            // Context isolation: global arrays are omitted
            extract($safeVars, EXTR_SKIP);
            include $filePath;
        } catch (\Throwable $e) {
            ob_end_clean();
            $this->log->error('Template runtime breakdown', ['exception' => $e]);
            throw new \RuntimeException('Execution failure', 0, $e);
        }

        return ob_get_clean() ?: '';
    }

    private function sanitizeVariables(array $vars): array
    {
        $allowed = ['title', 'userId'];
        $out = [];

        foreach ($allowed as $key) {
            if (isset($vars[$key])) {
                $value = filter_var($vars[$key], FILTER_SANITIZE_STRING);
                $out[$key] = substr($value, 0, 255);
            }
        }
        return $out;
    }
}

Infrastructure Hardening Guidelines

Input filtration is useless if the server container remains loose. Implement the following structural barriers:

Nginx Strict Directory Block

 # Deny direct browser execution of raw PHP files inside template storage
location /templates/ {
    location ~* \.(php|phar|phtml)$ {
        deny all;
    }
}

OS Least-Privilege Execution

  1. Strip write privileges from www-data over system binary paths like /usr/local/bin/
  2. Move command execution wrappers (like ffmpeg handlers) into dedicated, sandboxed system accounts isolated from the core web directory tree.

Made By Love ( Xia0checkmate ) — Bug bounty hunter — PHP Expert — Business Logic flaws expert

Follow me on Linkedin : https://www.linkedin.com/in/mohammed-zureigat/

Follow me on H1 : https://hackerone.com/xia0checkmate_jotp?type=user


메타데이터
post_id
f1b3fa5eacfe
slug
four-way-php-domino-fall-unchecked-input-to-full-root-compromise-f1b3fa5eacfe
url
https://medium.com/@Xiac0heckmate/four-way-php-domino-fall-unchecked-input-to-full-root-compromise-f1b3fa5eacfe
canonical_url
https://medium.com/@Xiac0heckmate/four-way-php-domino-fall-unchecked-input-to-full-root-compromise-f1b3fa5eacfe
author_url
https://medium.com/@Xiac0heckmate
status
ok
fetched_at
2026-06-09 15:37:30