前言

环境打包放再 https://github.com/sco4x0/huwangbei2018_easy_laravel 上了

其实这个题很想看一下师傅们的其它解法,因为猜到一定会有非预期解。

俊杰师傅带我了解了一下 phpggc 才发现自己还是太弱了 Orz

writeup

这是一个审计的题,在登录等页面查看源码,可以看到一段注释

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

得到代码后就可以开始审计了 (很多人都密我说代码没给全,拿到代码之后就算不了解laravel,看到composer.json也知道应该 composer install 一下呀)


首先看一下路由,看看有什么操作 routes/web.php

Route::get('/', function () { return view('welcome'); });
Auth::routes();
Route::get('/home', 'HomeController@index');
Route::get('/note', 'NoteController@index')->name('note');
Route::get('/upload', 'UploadController@index')->name('upload');
Route::post('/upload', 'UploadController@upload')->name('upload');
Route::get('/flag', 'FlagController@showFlag')->name('flag');
Route::get('/files', 'UploadController@files')->name('files');
Route::post('/check', 'UploadController@check')->name('check');
Route::get('/error', 'HomeController@error')->name('error');

可以看到 Auth::routes() ,这个路由是 Laravel 默认提供的一套关于用户系统的脚手架,使用php artisan make:auth即可开箱使用

具体的路由可以在 Illuminate/Routing/Router.php 中找到

/**
* Register the typical authentication routes for an application.
*
* @return void
*/
public function auth()
{
    // Authentication Routes...
    $this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
    $this->post('login', 'Auth\LoginController@login');
    $this->post('logout', 'Auth\LoginController@logout')->name('logout');

    // Registration Routes...
    $this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
    $this->post('register', 'Auth\RegisterController@register');

    // Password Reset Routes...
    $this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm');
    $this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
    $this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
    $this->post('password/reset', 'Auth\ResetPasswordController@reset');
}

或者直接在目录下 php artisan route:list ,这里也可以很方便的看到有auth, admin 这两个看起来很敏感的中间件名称

几乎所有操作都需要进行登录,而且 UploadControllerFlagController 都使用了一个叫做 admin 的中间件,在 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);
}

需要当前用户的邮箱为 admin@qvq.im ,这时发现使用这个邮箱是注册不上的,因为系统已经内置了这个管理员账号,但是看到 NoteController.php 中,存在一个非常明显的SQL注入漏洞

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

但是用户表中的密码是经过 bcrypt 处理的,注入出来没办法解开,但是在laravel5.4中,重置密码的操作很有意思 Illuminate\Auth\Passwords\PasswordBroker.php

首先是发送重置链接的方法

public function sendResetLink(array $credentials)
{
    // First we will check to see if we found a user at the given credentials and
    // if we did not we will redirect back to this current URI with a piece of
    // "flash" data in the session to indicate to the developers the errors.
    $user = $this->getUser($credentials);

    if (is_null($user)) {
        return static::INVALID_USER;
    }

    // Once we have the reset token, we are ready to send the message out to this
    // user with a link to reset their password. We will then redirect back to
    // the current URI having nothing set in the session to indicate errors.
    $user->sendPasswordResetNotification(
        $this->tokens->create($user)
    );

    return static::RESET_LINK_SENT;
}

可以看到如果存在这个用户的话,有个生成token的操作,实现在Illuminate\Auth\Passwords\DatabaseTokenRepository.php

public function create(CanResetPasswordContract $user)
{
    $email = $user->getEmailForPasswordReset();
    $this->deleteExisting($user);

    // We will create a new, random token for the user so that we can e-mail them
    // a safe link to the password reset form. Then we will insert a record in
    // the database so that we can verify the token within the actual reset.
    $token = $this->createNewToken();
    $this->getTable()->insert($this->getPayload($email, $token));
    return $token;
}
public function createNewToken()
{
    return hash_hmac('sha256', Str::random(40), $this->hashKey);
}
protected function getPayload($email, $token)
{
    return ['email' => $email, 'token' => $token, 'created_at' => new Carbon];
}

这里是直接把生成出来的token写入了数据库中,所以我在开头放了一个注入,本意就是注入出这个token去重置admin@qvq.im的密码,可是好像很多人都被users表里的remember_token误导了,这也是使用 Laravel 5.4的原因,在高于5.4的版本中,重置密码这个 token 会被 bcrypt 再存入,就和用户密码一样

重置密码使用管理员帐号登录后,会发现有 upload, filesflag 三个路由,通过查看 FlagController.php 可以发现直接就打印flag了

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

但是直接访问会发现页面提示 no flag,这里页面内容不一致,在 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);
}

而过期时间是依据文件的最后修改时间来判断的,所以判断服务器上编译后的文件最后修改时间大于原本模板文件,那么怎么去删除(修改)编译后的文件?

再来看看 UploadController 的上传

public function upload(UploadRequest $request)
{
    $file = $request->file('file');
    if (($file && $file->isValid())) {
        $allowed_extensions = ["bmp", "jpg", "jpeg", "png", "gif"];
        $ext = $file->getClientOriginalExtension();