2025-10-23 –, Europe
Livewire is a full-stack framework for Laravel that streamlines the creation of
dynamic and interactive web interfaces by allowing developers to build
real-time features using PHP and Blade templates. In this talk, we will show
how to exploit the unmarshalling mechanism used by Livewire to instantiate
arbitrary objects in order to achieve remote command execution on
any Livewire instance as long as you are in possession of the APP_KEY of the
application. Additionally, we will present a new feature added to our publicly
available tool laravel-crypto-killer, which fully automates the generation of
the payload described during the presentation.
Brief outline
- Introduction to Livewire
- Livewire unmarshalling process
- Synthesizers
- Checksum
- Building an unmarshelling chain from hydrators
- PHP magic methods
- First step : getting a phpinfo
- Second step: getting remote command execution
- Third step : make the server flaw stop to stay sneaky
- Exploitation by using laravel-crypto-killer
- Presentation of the freshly added exploit mode
- Showing the exploitation process on an actual project : Invoice Ninja
- Conclusion and thoughts
Detailed outline
Introduction
Livewire has gained significant popularity among Laravel developers due to its
simplicity and integration with Laravel's ecosystem. Additionally, strong community
support and compatibility with Laravel features make Livewire an attractive choice
for developers seeking to build modern, responsive web applications efficiently.
The application BuiltWith lists 676K instances of Laravel currently live
websites (BuiltWith-Laravel), and among them 106K instances of Livewire (15%)
(BuiltWith-Livewire). This makes Laravel one of the most used PHP frameworks
in the world, and Livewire one of its most used plugins.
This presentation will show how to get a remote command execution by abusing
the unmarshalling process of any Livewire instance, as long as we are in
possession of the APP_KEY of the application.
Livewire unmarshalling process
In a Livewire-based environment, a component is a class managing both the data
and the rendering logic, enabling real-time updates through properties and
methods that interact with the view. In order to manage each component state,
Livewire uses an unmarshalling mechanism described as hydration and
dehydration.
The data structure follows a principle where the last child nodes are
instantiated first, enabling the instantiation of each parent node. This
approach allows for multiple objects to be instantiated and nested within one
another.
+-- Grandchild 3
|
+-- Grandchild 2
|
+-- Child 2
|
| +-- Grandchild 1
| |
+-- Child 1
|
Parent
When a user interacts with a view, a POST request is sent to the server to update
the component state. The request looks like this:
POST /livewire/update HTTP/1.1
Host: livewire.local
[...]
{
"_token":"jMEN2kTQRrwSA5CgH5y8WWqbCpdb4Lx4iBznnlFD",
"components":[
{
"snapshot":"{\"data\":{\"count\":null},\"memo\":{\"id\":\"Y6a883cdUFy82whZ10JW\",\"name\":\"counter\",\"path\":\"counter\",\"method\":\"GET\",\"children\":[],\"scripts\":[],\"assets\":[],\"errors\":[],\"locale\":\"en\"},\"checksum\":\"f56c273c0e4a3eaa5d7fdea9e7142c42d0e1128a8aee35e9546baffaa41870ac\"}",
"updates":{},
"calls":[
{
"path":"",
"method":"increment",
"params":[]
}
]
}
]
}
In this request, two fields are particularly important. First, the
components->snapshot
field contains all the serialized information needed to
restore the component's state on the server side, including the properties and
their values. Second, the components->calls
field defines the list of methods
that need to be called on the component, along with any associated parameters.
Inside the components->snapshot->data
field, properties are defined via
synthesizers. Synthesizers are identified through a special "s" field
inside the data structure. They are a powerful feature that extends Livewire’s
capability to handle more complex property types that cannot be serialized
natively, such as Eloquent models, Laravel collections, Carbon date instances,
or custom user-defined types.
Livewire synthesizers
Synthesizers provide a mechanism to define how these custom types should be
JSON-serialized (dehydrated) and JSON-deserialized (hydrated) when sent between
the client and server. This ensures that the state of these properties is
correctly maintained across requests.
Here are some examples of default synthesizers:
- str: A Stringable object is hydrated and dehydrated as its string
representation. - arr: A simple PHP array is hydrated and dehydrated without transformation.
- std: A standard stdClass object is hydrated and dehydrated by treating its
properties as an associative array. - clctn: A Laravel collection is hydrated and dehydrated by converting it to
and from arrays, can be called on any object loaded in PHP. - Etc.
CollectionSynth
In the context of Livewire, many hydrators will allow a user to call constructors
on arbitrary object.
For example, the CollectionSynth
class is used to manage how collection-like
objects are handled during the component dehydration and hydration processes.
Its role is to ensure that PHP collections (such as Laravel’s Collection
instances)
are properly reconstructed.
1 <?php
2
3 namespace Livewire\Mechanisms\HandleComponents\Synthesizers;
4
5 class CollectionSynth extends ArraySynth {
6 public static $key = 'clctn';
7 [...]
8 function hydrate($value, $meta, $hydrateChild) {
9 foreach ($value as $key => $child) {
10 $value[$key] = $hydrateChild($key, $child);
11 }
12 return new $meta['class']($value);
13 }
14 }
The $key
(line 6) is set to the 'clctn' value described earlier as the
synthesizer identifier.
When the hydrate
method is called (line 8), it receives a $value
, which
represents the serialized collection data sent by the user, a $meta
array
containing metadata also controlled by the user, and a $hydrateChild
callback used to individually process each embedded element of the collection.
Once all elements are processed, a new instance of the original collection
class is created using the reconstructed array by using new $meta['class']($value)
which is controlled by the user, allowing an arbitrary object instantiation.
Checksum
A protection has been put in place, in order to make sure that users
do not temper with the synthesizers and managed objects. Before sending
update
requests, Livewire generates a checksum (or hash) based on the data
sent to the frontend. This checksum is created using the secure hashing
algorithm SHA-256 and the Laravel APP_KEY
. It includes the data used to
validate their integrity.
The checksum is verified on each request sent by the user, so if the
data is modified, the checksum won't be correct.
But what if the APP_KEY
was leaked? We already published research dedicated
to this subject: Deep dive in Laravel
encryption,
and our conclusion was that many APP_KEY
s are already leaked from GitHub, or
are default ones. Therefore, this chain of exploitation is in the continuity of
our previous work.
Building an unmarshelling chain from hydrators
Thanks to hydration mechanisms, we identified a unmarshelling chain allowing
users to get remote command execution on any Livewire application, providing
the APP_KEY
is in our possession. The three main steps leading to this RCE
will be detailed.
First step : getting a phpinfo
- Detailed chain
- Analysis of the
GuzzleHttp\Psr7\FnStream
class sources - Analysis of the
League\Flysystem\UrlGeneration\ShardedPrefixPublicUrlGenerator
class sources - Payload building on Livewire
- Analysis of the
Second step: getting a remote command execution
- Detailed chain
- Analysis of the
Laravel\SerializableClosure\Serializers\Serializable
class sources - Analysis of the
Illuminate\Bus\Queueable
Trait sources - Analysis of the
Illuminate\Broadcasting\BroadcastEvent
class sources - Payload building on Livewire leading to RCE
- Analysis of the
- Problem : the flow generates an error 500 even if the RCE is reached
Third step: make the server flow stop to stay sneaky
- Rebuilding the previously used gadget chain to continue the application flow
after theunserialize
- Analysys of the
Laravel\Prompts\Terminal
class allowing to reach anexit
call
Exploitation by using laravel-crypto-killer
A module was developed to automate all the process inside
laravel-crypto-killer, a new exploit
mode is available, fully
automating the exploit payload generation detailed in this presentation.
Common Laravel based projects using Livewire are affected, such as invoiceninja,
which has a default APP_KEY
, making it vulnerable to this exploit by default.
Rémi Matasse
I am Rémi Matasse (pseudo Remsio), a pentester that worked at Synacktiv for the past four years, passionated by offensive web security, especially on anything related to PHP.
I passed some years working on concrete PHP filters chain exploitation, documenting it in blogpost and presenting it in several conferences such as Pass The Salt or hack.lu.
I then decided to focus on the Laravel since we often come across this framework during audits before jumped in with both feet on exploitation based on APP_KEY leaks.
Pierre Martin
My name is Pierre Martin (pseudo Worty), I'm 24 and I've been doing cybersecurity for about 3 years. I take part in a lot of CTFs with the TheFlatNetworkSociety team and I specialize in the web category, mainly on the backend side.
Before becoming a pentester at Synacktiv, I did a lot of bug bounty on YesWeHack and HackerOne, and I had the opportunity to take part in the HackerOne world championship with the French team, where we finished third.
Moreover, I was twice in the French team for the ECSC competition organized every year.
I mainly do vulnerability research on opensource projects, on my own time or at work, notably with Rémi Matasse.
Passionated by offensive web security and more specifically anything related to backend languages.