除前面文章写过的 传统的unserialize()函数来调用反序列化,phar触发反序列化、还有一种方法是 利用session 来进行反序列化、或文件包含。因此通过这篇文章,来看一下如何利用 session
目录
<1> session是什么?
在利用之前,我们先来了解一下 什么是session
session的本质和cookie是差不多的,保存着http状态信息。简单来说、Session是一次浏览器和服务器的交互的会话
那么我们说了,什么是会话? 比如我们问别人:How are you? 他们回答: Fine thank u。 这就是一次会话。而cookie和session是客户机与服务器之间的会话产生的。客户机和服务器每进行一次会话会记录一个值赋给$_SESSION['name'],这也是为什么,如果你在登录页面输入了账号密码,当它眺转到另一个页面也是以你之前的这个登录状态执行的。
但有cookie了,为什么还会出现Session会话呢?
因为我们用浏览器访问网站用的是
http
协议,http
协议是一种无状态的协议,就是说它不会储存任何东西,每一次的请求都是没有关联的,无状态的协议好处就是快速;但它也有不方便的地方,比如说我们在
login.php
登录了,我们肯定希望在index.php
中也是登录的状态,否则我们点开其他服务的时候又需要登录一次,那在login.php处登录还有什么意义呢?但前面说到了http
协议是无状态的协议,那访问两个页面就是发起两个http
请求,他们俩之间是无关联的,所以无法单纯的在index.php中读取到它在login.php中已经登陆了的状态信息。为了解决这个问题,
cookie
就诞生了,cookie
是把少量数据存在客户端,它在一个域名下是全局的,相当于php
可以在这个域名下的任何页面读取cookie
信息,那只要我们访问的两个页面在同一个域名下,那就可以通过cookie
获取到登录信息了。
cookie虽然避免了前面提到的 重复登录的问题,但是cookie存在一定的不安全性。因为cookie
是存在于客户端的,那用户就是可见的,并且可以修改的;那如何又要安全,又可以全局读取信息呢?那么,Session就出现了,其实它的本质和cookie
是一样的,只不过它是存在于服务器端的 更安全
<2> session的产生和存储
前面提到他是会话过程中产生的,那具体是怎么产生的呢?
这里我们说一下php中的Session机制
说到这个,肯定离不开 session_start()函数,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的。
<?php
highlight_file(__FILE__);
session_start();
echo "session_id 为: ".session_id()."<br>";
echo "COOKIE 为: ".$_COOKIE["PHPSESSID"];
这里可以看出session_id()
这个系统方法是输出了本次生成的session_id
,并且存入了COOKIE
中,参数名为PHPSESSID
,如果我们不关闭浏览器,只刷新的话,它的值都是不变的,但当你关掉浏览器之后,重新打开 会生成一个新的session_id
相比于把账户密码存在cookie中,这种一个周期的存储更加安全
session_id的值存储在服务器的临时目录tmp/temp下. 通过配置文件 php.ini 可以看见,这里我们看看本地phpstudy_pro session_id的值存储在哪个文件路径。
可以看见 他的存储文件格式为:sess_ + session_id
session的一些默认存储位置
/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/var/lib/php5/sess_PHPSESSID
/var/lib/php5/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
虽然说session存储在服务器里,那我们能不能通过修改COOKIE
中PHPSESSID
的值来修改session_id
呢?
成功更改 同时,看一下本地保存的路径下 也出现了sess_123文件
但是 文件是0Kb 里面什么东西都没有 空文件肯定是没用的 那我们怎么样往里写入一些东西呢
<3> session的写入和读取
在php中,session的使用是通过预定义数组$_session的调用和读取来完成。
在网站的页面中,在注册页面对$_session数组进行赋值,在其他的页面中对$_session数组进行读取。我们更改之前的代码,加上数组赋值语句
可以看见执行完之后,sess_123文件里写入了赋值的内容。格式为 键名|序列化数据
我们看见| 之后那类似php序列化格式串里的数据,这不就和反序列化联系起来了吗🤩
大概梳理一下前三小节的铺垫内容:
当我们在浏览器中,HTTP请求一个页面时,其中后端php执行session_start()会打开一个会话,此时会寻找COOKIE中的PHPSESSID 是否为空,若不为空session_id()就是这个值,若为空则随机生成一个32位session_id存入PHPSESSID中,然后在对应临时文件路径下生成一个文件名为sess_+session_id 的文件以及将$_SESSION的键值对按照一定格式存储到文件中
终于引入了正题,让我们继续往下看
<4> php中session相关配置
看一下phpinfo()里 session有关选项
session.auto_start,这个开关是指定是否在请求开始时就自动启动一个会话,默认为Off;如果它为 On
的话,相当于就先执行了一个session_start()
,会生成一个session_id 便不用再在php里写入session_start()
session.save_path 里面是sess_session_id 文件的保存路径
session.save_handler 里面是用户自定义session存储的选项,默认是files 即文件形式存储
session.serialize_handler 里面是序列化存储格式
这个是至为重要的一环:php存储session有三种模式,php_serialize, php_binary
让我们依次来看一下这三种模式 的区别
首先看php 也是默认的
上文中我们其实已经见过了 ,它的规则是$_SESSION是个数组,数组中的键和值中间用 | 来分割,值如果是数组或对象按照序列化的格式存储
再看php_serialize模式
注意,php_serialize在5.5版本后新加的一种规则,5.4及之前版本,如果设置成php_serialize会报错
session.serialize_handler = php 一直都在 它是用 |分割
session.serialize_handler = php_serialize 5.5之后启用 它是用serialize反序列化格式分割
首先我们还是得先用ini_set
进行设置,语句如下:
ini_set('session.serialize_handler','php_serialize');
根据报错,我们需要在保证session会话未激活时设置 放在session_start()前面
查看sess_session_id文件,可以看见这个是完全按照 php序列化来存储的,键名也包含在其中,返回回来一个数组
再看 php_binary
这里我们还是得先用ini_set
进行设置,语句如下:
ini_set('session.serialize_handler','php_binary');
这个处理器的格式是键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理后的值;注意这个键名的长度所所对应的ASCII字符,就比如说键名长度为4,那它对应的就是ASCII码为4的字符,是个不可见字符EOT
因此我们看见的 □ 就是这个键名的长度5所所对应的ASCII字符。
<5>session反序列化原理
(1) 原理介绍
上文提到了那么多,什么是session,session的产生、写入、读取,session存储文件内容格式为序列化数据等等,都没提到说反序列化,那为什么会产生session反序列化漏洞呢?
类似于phar:// 会对文件里序列化数据自动进行反序列化,Session
反序列化也不需要unserialize()
函数就可以实现。怎么实现的呢,让我们看一下session_start()函数介绍:
我们可以看到官方解释:当session_start重用现有会话时,php内部调用会话管理器的read函数,通过read回调函数调用现有会话数据(文件里序列化内容),php会自动反序列化数据 填充到$_SESSION 数组中
那就好说了,会调用反序列化的话,那我们是不是 可以通过写入自己构造的序列化数据到sess_sessid 文件里,然后在有反序列化漏洞页面刷新页面,由于这个页面依然有session_start()
,那它就去读取那个文件的内容,从而自动进行反序列化呢?
当然可以。不过问题又来了,我们怎么去控制sess_sessid 文件里的内容呢?
这里就要利用到两种模式的区别了,虽然上文提到的 php模式、php_serialize 两个模式的2个序列化格式本身没有问题,但是如果2个混合起用就会造成危害。
形成原理:是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时 "|"会被当成键值对的分隔符
干说可能看起来比较晕,这里我们实际操作一下大家就明白了:
(2) 实例分析
写入一个test1.php 里面包含一个Test类,当他被反序列化时会调用__wakeup() 造成eval执行命令
#test1.php
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php');
session_start();
class Test{
public $code;
function __wakeup(){
eval($this->code);
}
}
?>
再看test2.php
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
if(isset($_GET['test'])){
$_SESSION['test']=$_GET['test'];
echo session_id();
}
?>
在test2.php中接受用户传入的参数并储存至session中,序列化处理器为php_serialize
比如我们想利用 session反序列化 执行phpinfo() 那我们应该在sess_id 存储文件里写入的序列化内容为:O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";}
首先我们通过test2.php 将" | " + O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";} 通过GET传参传入到$_SESSION数组,由于是php_serialize模式,sessid存储文件里内容为:
可以看见文件里成功写入。如果此时我们再用test1.php的 php 模式去读取,我们知道php模式下 sess_id存储文件的内容格式为:键名 + |分隔符 + 序列化数据。那么此时会把我们构造的O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";} 当初序列化数据来反序列化,那便会执行phpinfo命令
我们来试一下:
成功执行phpinfo() 这就是session反序列化利用的原理