OSCP study materials
OSCP study resources

Local File Inclusion With the Obvious Paths Closed

OSCP · Put your knowledge to work in context.

All case studies

Local File Inclusion With the Obvious Paths Closed

On app.target.local (Ubuntu 20.04, PHP 7.4, Apache) you find GET /download.php?file=report.pdf, which renders the file inline. Traversal works: ../../../../etc/passwd returns the file. Reading the source via php://filter returns include($_GET['file']) and php.ini confirms allow_url_include=Off, so php://input and data:// wrappers return "URL file-access is disabled". You can read /var/log/apache2/access.log, but your User-Agent injections never appear in it - the app sits behind a reverse proxy that logs elsewhere, and the file is not writable by the web user anyway. A second feature, POST /upload.php, accepts an avatar: it checks the extension is .jpg and that the first bytes are FF D8 FF, then stores the file at /var/www/app/uploads/ using the client-supplied filename.

1. Given the dead ends, choose your escalation route and justify why it beats the alternatives.

Read the worked answer

The upload directory is the route, because the primitive you have is include(), not code execution.

PHP's include does not care about file extensions or Content-Type. If a file inside the include path contains <?php ... ?>, those tags are executed. So a file that begins with FF D8 FF (passing the byte check), carries a .jpg name (passing the extension check), and then contains a PHP block satisfies every control the uploader implements and is still executed by the include.

Why not the alternatives:

  • Log poisoning is disproven, not merely unlikely: you read the log and your payload is absent, so the process writing it is on another host. Any further time here is wasted.
  • php://input / data:// are disabled by allow_url_include=Off. Confirmed by error, not assumption.
  • PHP filter chains (generate a filter chain that synthesises PHP from php://temp) work with no write primitive and beat open_basedir, but they need large memory limits, produce very conspicuous multi-kilobyte URLs, and are fragile. Keep as fallback, not first choice.

Before uploading, verify with the LFI that /var/www/app/uploads/ is readable by the web user and check open_basedir in php.ini. If open_basedir is set to /var/www/app, an upload path inside it is still fine - that is exactly why the directory matters.

2. You have uploaded avatar.jpg containing a PHP payload. What do you test first, and what do you do if the include returns the raw file contents instead of executing it?

Read the worked answer

Test with a harmless payload that proves execution rather than shell delivery: <?php echo 'MARKER-'.php_uname(); ?>. Then request /download.php?file=/var/www/app/uploads/avatar.jpg. Two outcomes matter.

Executed: you have code execution as the web user. Immediately establish whether the box is a container (/.dockerenv, cgroup contents) before planning privilege escalation - a container changes what "root" is worth and where credentials live.

Raw contents returned: the file was found and read but not parsed, which almost always means include is not what you think - check the source again for readfile(), file_get_contents() or an include behind a path prefix that only appends .php. If the source truly includes, then check the PHP handler configuration: a per-directory php_admin_flag engine off in the uploads vhost or a .htaccess/php-fpm pool setting can disable parsing for that path.

Fix in that case: upload to a different directory the app accepts (nested traversal in the filename, e.g. ../cache/x.jpg if the name is used unsanitised), or exploit the same include against a file that is definitely parsed, such as a session file under /var/lib/php/sessions/ if session.save_path is readable and writable by the app.